[IMP] *: re-add a bunch of tests

This commit is contained in:
Samuel Degueldre
2021-10-21 10:13:08 +02:00
committed by Aaron Bohy
parent b6eb4d009e
commit ced5d0f69f
19 changed files with 1525 additions and 279 deletions
+2 -2
View File
@@ -16,12 +16,12 @@ export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void |
export function onMounted(fn: () => void | any) {
const node = getCurrent()!;
node.mounted.push(fn);
node.mounted.unshift(fn);
}
export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent()!;
node.willPatch.push(fn);
node.willPatch.unshift(fn);
}
export function onPatched(fn: () => void | any) {
+1 -1
View File
@@ -19,7 +19,7 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
return {
get el(): HTMLElement | null {
const val = node.refs[name];
return val!;
return val || null;
// if (val instanceof HTMLElement) {
// return val;
// } else if (val instanceof Component) {
@@ -134,6 +134,34 @@ exports[`basics can handle empty props 2`] = `
}"
`;
exports[`basics can inject values in tagged templates 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].n;
return block1([d1]);
}
}"
`;
exports[`basics can inject values in tagged templates 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
const callTemplate_2 = getTemplate(\`__template__9\`);
return function template(ctx, node, key = \\"\\") {
return callTemplate_2(ctx, node, key + \`__1\`);
}
}"
`;
exports[`basics can mount a component with just some text 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1006,6 +1034,36 @@ exports[`basics updating a component with t-foreach as root 1`] = `
}"
`;
exports[`basics updating widget immediately 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<span>abc<block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['props'].flag) {
b2 = text(\`def\`);
}
return block1([], [b2]);
}
}"
`;
exports[`basics updating widget immediately 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {flag: ctx['state'].flag}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`basics widget after a t-foreach 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1074,3 +1132,71 @@ exports[`basics zero or one child components 2`] = `
}
}"
`;
exports[`support svg components add proper namespace to svg 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<g><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`support svg components add proper namespace to svg 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<svg><block-child-0/></svg>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`GComp\`, {}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`t-raw in components can render list of t-raw 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['state'].items);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`item\`] = v_block2[i1];
let key1 = ctx['item'];
let b4 = text(ctx['item']);
let b5 = html(ctx['item']);
c_block2[i1] = withKey(multi([b4, b5]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`t-raw in components update properly on state changes 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = html(ctx['state'].value);
return block1([], [b2]);
}
}"
`;
@@ -76,3 +76,28 @@ exports[`event handling support for callable expression in event handler 1`] = `
}
}"
`;
exports[`event handling t-on with handler bound to dynamic argument on a t-foreach 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div class=\\"item\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['items']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`item\`] = v_block2[i1];
let key1 = ctx['item'];
const arg1 = [ctx['item']];
let d1 = [ctx, 'onClick', arg1];
c_block2[i1] = withKey(block3([d1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
@@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`hooks autofocus hook input in a t-if 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><input block-ref=\\"0\\"/><block-child-0/></div>\`);
let block2 = createBlock(\`<input block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs;
let b2;
let d1 = (el) => refs[\`input1\`] = el;
if (ctx['state'].flag) {
let d2 = (el) => refs[\`input2\`] = el;
b2 = block2([d2]);
}
return block1([d1], [b2]);
}
}"
`;
exports[`hooks autofocus hook simple input 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><input block-ref=\\"0\\"/><input block-ref=\\"1\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs;
let d1 = (el) => refs[\`input1\`] = el;
let d2 = (el) => refs[\`input2\`] = el;
return block1([d1, d2]);
}
}"
`;
exports[`hooks can use onWillStart, onWillUpdateProps 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['props'].value;
return block1([d1]);
}
}"
`;
exports[`hooks can use onWillStart, onWillUpdateProps 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`MyComponent\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`hooks can use useComponent 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`hooks mounted callbacks should be called in reverse order from willUnmount callbacks 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>hey<block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].value;
return block1([d1]);
}
}"
`;
exports[`hooks two different call to willPatch/patched should work 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>hey<block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].value;
return block1([d1]);
}
}"
`;
exports[`hooks useRef hook: basic use 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><button block-ref=\\"0\\"><block-text-1/></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs;
let d1 = (el) => refs[\`button\`] = el;
let d2 = ctx['value'];
return block1([d1, d2]);
}
}"
`;
@@ -14,6 +14,99 @@ exports[`lifecycle hooks basic checks for a component 1`] = `
}"
`;
exports[`lifecycle hooks component semantics 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>B</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks component semantics 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>D</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks component semantics 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>E</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks component semantics 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>F</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks component semantics 5`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>C<block-child-0/><block-child-1/><block-child-2/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b3,b4;
b2 = component(\`D\`, {}, key + \`__1\`, node, ctx);
if (ctx['state'].flag) {
b3 = component(\`E\`, {}, key + \`__2\`, node, ctx);
} else {
b4 = component(\`F\`, {}, key + \`__3\`, node, ctx);
}
return block1([], [b2, b3, b4]);
}
}"
`;
exports[`lifecycle hooks component semantics 6`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div>A<block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`B\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`C\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`lifecycle hooks components are unmounted and destroyed if no longer in DOM 1`] = `
"function anonymous(bdom, helpers
) {
+217
View File
@@ -741,6 +741,165 @@ describe("basics", () => {
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div><span>asdf</span><span>asdf</span></div></div>");
});
// TODO: rename
test("updating widget immediately", async () => {
// in this situation, we protect against a bug that occurred: because of the
// interplay between components and vnodes, a sub widget vnode was patched
// twice.
class Child extends Component {
static template = xml`<span>abc<t t-if="props.flag">def</t></span>`;
}
class Parent extends Component {
static template = xml`<Child flag="state.flag"/>`
static components = { Child };
state = useState({ flag: false });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>abc</span>");
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<span>abcdef</span>");
});
test("can inject values in tagged templates", async () => {
const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`;
class Parent extends Component {
static template = xml`<t t-call="${SUBTEMPLATE}"/>`;
state = useState({ n: 42 });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>42</span>");
});
// Depends on t-props
test.skip("update props of component without concrete own node", async () => {
class Custom extends Component {
static template = xml`
<div class="widget-subkey">
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
</div>`;
}
class Child extends Component {
static components = { Custom };
static template = xml`
<Custom
t-key="props.subKey"
key="props.key"
subKey="props.subKey"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<Child t-key="childProps.key" t-props="childProps"/>
</div>`;
childProps = {
key: 1,
subKey: 1,
};
}
const parent = await mount(Parent, fixture);
expect(fixture.textContent!.trim()).toBe("1__1");
// First step: change the Custom's instance
Object.assign(parent.childProps, {
subKey: 2,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("1__2");
// Second step, change both Child's and Custom's instance
Object.assign(parent.childProps, {
key: 2,
subKey: 3,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test.skip("subcomponents cannot change observable state received from parent", async () => {
const consoleError = console.error;
console.error = jest.fn();
class Child extends Component {
static template = xml`<div/>`;
setup() {
this.props.obj.coffee = 2;
}
}
class Parent extends Component {
static template = xml`<div><Child obj="state.obj"/></div>`;
static components = { Child };
state = useState({ obj: { coffee: 1 } });
}
let error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe('Observed state cannot be changed here! (key: "coffee", val: "2")');
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
});
describe("dynamic t-props", () => {
test.skip("basic use", async () => {
expect.assertions(4);
class Child extends Component {
static template = xml`
<span>
<t t-esc="props.a + props.b"/>
</span>
`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2 });
expect(this.props).not.toBe(parent.some.obj);
}
}
class Parent extends Component {
static template = xml`
<div>
<Child t-props="some.obj"/>
</div>
`;
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
});
test.skip("t-props with props", async () => {
expect.assertions(1);
class Child extends Component {
static template = xml`<div />`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2, c: "c" });
}
}
class Parent extends Component {
static template = xml`
<div>
<Child t-props="props" a="1" b="2" />
</div>
`;
static components = { Child };
props = { a: "a", c: "c" };
}
await mount(Parent, fixture);
});
});
describe.skip("mount targets", () => {
@@ -1012,3 +1171,61 @@ describe.skip("mount special cases", () => {
// expect(fixture.textContent).toBe("fixedsome text");
});
});
describe("support svg components", () => {
test("add proper namespace to svg", async () => {
class GComp extends Component {
static template = xml`
<g>
<circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/>
</g>`;
}
class Svg extends Component {
static template = xml`
<svg>
<GComp/>
</svg>`;
static components = { GComp };
}
await mount(Svg, fixture);
expect(fixture.innerHTML).toBe(
'<svg><g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle></g></svg>'
);
});
});
describe("t-raw in components", () => {
test("update properly on state changes", async () => {
class Test extends Component {
static template = xml`<div><t t-raw="state.value"/></div>`;
state = useState({ value: "<b>content</b>" });
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div><b>content</b></div>");
component.state.value = "<span>other content</span>";
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>other content</span></div>");
});
test("can render list of t-raw ", async () => {
class Test extends Component {
static template = xml`
<div>
<t t-foreach="state.items" t-as="item" t-key="item">
<t t-esc="item"/>
<t t-raw="item"/>
</t>
</div>`;
state = useState({ items: ["<b>one</b>", "<b>two</b>", "<b>tree</b>"] });
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe(
"<div>&lt;b&gt;one&lt;/b&gt;<b>one</b>&lt;b&gt;two&lt;/b&gt;<b>two</b>&lt;b&gt;tree&lt;/b&gt;<b>tree</b></div>"
);
});
});
+20
View File
@@ -302,4 +302,24 @@ describe("event handling", () => {
button.click();
expect(steps).toEqual(["captured", "normal"]);*/
});
test.only("t-on with handler bound to dynamic argument on a t-foreach", async () => {
expect.assertions(3);
class Parent extends Component {
static template = xml`
<div>
<t t-foreach="items" t-as="item" t-key="item">
<div class="item" t-on-click="onClick(item)"/>
</t>
</div>`;
items = [1, 2, 3, 4];
onClick(n: number, ev: MouseEvent) {
expect(n).toBe(1);
expect(ev).toBeInstanceOf(MouseEvent);
}
}
await mount(Parent, fixture);
(<HTMLElement>fixture.querySelector('.item')).click();
});
});
+303
View File
@@ -0,0 +1,303 @@
import { App, Component, mount, useRef, useState, useComponent, onMounted, onPatched, onWillStart, onWillUpdateProps, onWillPatch, xml, onWillUnmount } from "../../src";
import {
makeTestFixture,
nextTick,
snapshotEverything,
} from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("hooks", () => {
test("useRef hook: basic use", async () => {
let counter: Counter;
class Counter extends Component {
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
button = useRef("button");
value = 0;
setup() {
counter = this;
}
increment() {
this.value++;
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
}
}
const mounted = mount(Counter, fixture);
expect(counter!.button.el).toBe(null);
await mounted;
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
expect(counter!.button.el).not.toBe(null);
expect(counter!.button.el).toBe(fixture.querySelector("button"));
counter!.increment();
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
});
// TODO: rename like next test (ensures willPatch/patched calls are symmetrical)
test("two different call to willPatch/patched should work", async () => {
const steps: string[] = [];
function useMyHook(i: number) {
onPatched(() => {
steps.push("hook:patched" + i);
});
onWillPatch(() => {
steps.push("hook:willPatch" + i);
});
}
class Test extends Component {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
setup() {
useMyHook(1);
useMyHook(2);
}
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>hey1</div>");
component.state.value++;
await nextTick();
expect(fixture.innerHTML).toBe("<div>hey2</div>");
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
});
test("mounted callbacks should be called in reverse order from willUnmount callbacks", async () => {
const steps: string[] = [];
function useMyHook(i: number) {
onMounted(() => {
steps.push("hook:mounted" + i);
});
onWillUnmount(() => {
steps.push("hook:willUnmount" + i);
});
}
class Test extends Component {
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
state = useState({ value: 1 });
setup() {
useMyHook(1);
useMyHook(2);
}
}
const app = new App(Test);
await app.mount(fixture);
app.destroy();
expect(steps).toEqual(["hook:mounted2", "hook:mounted1", "hook:willUnmount1", "hook:willUnmount2"]);
});
describe("autofocus hook", () => {
function useAutofocus(name: string) {
let ref = useRef(name);
let isInDom = false;
function updateFocus() {
if (!isInDom && ref.el) {
isInDom = true;
ref.el.focus();
} else if (isInDom && !ref.el) {
isInDom = false;
}
}
onPatched(updateFocus);
onMounted(updateFocus);
}
test("simple input", async () => {
class Test extends Component {
static template = xml`
<div>
<input t-ref="input1"/>
<input t-ref="input2"/>
</div>`;
setup() {
useAutofocus("input2");
}
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div><input><input></div>");
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
test("input in a t-if", async () => {
class Test extends Component {
static template = xml`
<div>
<input t-ref="input1"/>
<t t-if="state.flag"><input t-ref="input2"/></t>
</div>`;
state = useState({ flag: false });
setup() {
useAutofocus("input2");
}
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div><input></div>");
expect(document.activeElement).toBe(document.body);
component.state.flag = true;
await nextTick();
const input2 = fixture.querySelectorAll("input")[1];
expect(input2).toBe(document.activeElement);
});
});
test.skip("can use useEnv", async () => {
expect.assertions(2);
class Test extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
setup() {
//expect(useEnv()).toBe(this.env);
}
}
await mount(Test, fixture);
});
test.skip("can use sub env", async () => {
class Test extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
setup() {
//useSubEnv({ val: 3 });
}
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(component.env).not.toHaveProperty("val");
expect(component.env).toHaveProperty("val");
});
test("can use useComponent", async () => {
expect.assertions(2);
class Test extends Component {
static template = xml`<div></div>`;
setup() {
expect(useComponent()).toBe(this);
}
}
await mount(Test, fixture);
});
test.skip("parent and child env", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="env.val"/></div>`;
super() {
//useSubEnv({ val: 5 });
}
}
class Parent extends Component {
static template = xml`<t t-esc="env.val"/><Child/>`;
static components = { Child };
setup() {
//useSubEnv({ val: 3 });
}
}
mount(Parent, fixture);
expect(fixture.innerHTML).toBe("3<div>5</div>");
});
test("can use onWillStart, onWillUpdateProps", async () => {
const steps: string[] = [];
async function slow(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("slow");
}, 0);
});
}
function useMyHook() {
onWillStart(async () => {
steps.push(await slow());
steps.push("onWillStart");
});
onWillUpdateProps(async (nextProps) => {
expect(nextProps).toEqual({ value: 2 });
steps.push(await slow());
steps.push("onWillUpdateProps");
});
}
function use2ndHook() {
onWillStart(() => {
steps.push("on2ndStart");
});
onWillUpdateProps((nextProps) => {
expect(nextProps).toEqual({ value: 2 });
steps.push("on2ndUpdate");
});
}
class MyComponent extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
setup() {
useMyHook();
use2ndHook();
}
}
class App extends Component {
static template = xml`<MyComponent value="state.value"/>`;
static components = { MyComponent };
state = useState({ value: 1 });
}
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<span>1</span>");
// NOTE: 'on2ndStart' appears first in the list even though
// the 'use2ndHook' is declared after 'useMyHook'. This is
// because Promise.all is used to call the callbacks specified
// in the hooks, which runs them simultaneously.
// Additionally, 'slow' should be listed before 'onWillStart'
// because call to `slow` is awaited.
expect(steps).toEqual(["on2ndStart", "slow", "onWillStart"]);
app.state.value = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<span>2</span>");
expect(steps).toEqual([
"on2ndStart",
"slow",
"onWillStart",
"on2ndUpdate",
"slow",
"onWillUpdateProps",
]);
});
test.skip("useExternalListener", async () => {
let n = 0;
class MyComponent extends Component {
static template = xml`<span><t t-esc="props.value"/></span>`;
setup() {
//useExternalListener(window as any, "click", this.increment);
}
increment() {
n++;
}
}
class App extends Component {
static template = xml`<MyComponent t-if="state.flag"/>`;
static components = { MyComponent };
state = useState({ flag: false });
}
const app = await mount(App, fixture);
expect(n).toBe(0);
window.dispatchEvent(new Event("click"));
expect(n).toBe(0);
app.state.flag = true;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
app.state.flag = false;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
});
});
+95
View File
@@ -969,4 +969,99 @@ describe("lifecycle hooks", () => {
"hook:willunmount1",
]);*/
});
// TODO: rename (corresponds to https://github.com/odoo/owl/blob/master/doc/reference/concurrency_model.md#semantics)
test("component semantics", async () => {
let steps: string[] = [];
class TestWidget extends Component {
name: string = "test";
setup() {
useLogLifecycle(steps);
}
}
class B extends TestWidget {
static template = xml`<div>B</div>`;
name = "B";
}
class D extends TestWidget {
static template = xml`<div>D</div>`;
name = "D";
}
class E extends TestWidget {
static template = xml`<div>E</div>`;
name = "E";
}
class F extends TestWidget {
static template = xml`<div>F</div>`;
name = "F";
}
let c: C;
class C extends TestWidget {
static template = xml`
<div>C<D />
<E t-if="state.flag" />
<F t-else="!state.flag" />
</div>`;
static components = { D, E, F };
name = "C";
state = useState({ flag: true });
setup() {
c = this;
super.setup();
}
}
class A extends TestWidget {
static template = xml`<div>A<B /><C /></div>`;
static components = { B, C };
name = "A";
}
await mount(A, fixture);
expect(fixture.innerHTML).toBe(`<div>A<div>B</div><div>C<div>D</div><div>E</div></div></div>`);
expect(steps).toEqual([
"A:setup",
"A:willStart",
"A:render",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"B:render",
"C:render",
"D:setup",
"D:willStart",
"E:setup",
"E:willStart",
"D:render",
"E:render",
"E:mounted",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]);
// update
steps.splice(0);
c!.state.flag = false;
await nextTick();
expect(steps).toEqual([
"C:render",
"D:willUpdateProps",
"F:setup",
"F:willStart",
"D:render",
"F:render",
"C:willPatch",
"D:willPatch",
"E:willUnmount",
"E:destroyed",
"F:mounted",
"D:patched",
"C:patched",
]);
});
});
+100
View File
@@ -44,4 +44,104 @@ describe("refs", () => {
'<div><span class="counter">1</span><span><button>do something</button></span></div>'
);
});
// TODO: rename
test.skip("t-refs on widget are components", async () => {
class Child extends Component {
static template = xml`<div>b</div>`;
}
let parent: Parent;
class Parent extends Component {
static template = xml`<div class="outer-div">Hello<Child t-ref="mywidgetb" /></div>`;
static components = { Child };
ref = useRef<Child>("mywidgetb");
setup() {
parent = this;
}
}
const mounted = mount(Parent, fixture);
expect(parent!.ref.comp).toBe(null);
expect(parent!.ref.el).toBe(null);
await mounted;
expect(parent!.ref.comp).toBeInstanceOf(Child);
expect(parent!.ref.el).toEqual(fixture.querySelector(".outer-div > div"));
});
test.skip("t-refs are bound at proper timing", async () => {
expect.assertions(4);
class Child extends Component {
static template = xml`<div>widget</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child t-foreach="state.list" t-as="elem" t-ref="child" t-key="elem"/>
</div>
`;
static components = { Child };
state = useState({ list: <any>[] });
child = useRef("child");
willPatch() {
expect(this.child.comp).toBeNull();
}
patched() {
expect(this.child.comp).not.toBeNull();
}
}
const parent = await mount(Parent, fixture);
parent.state.list.push(1);
await nextTick();
});
test.skip("t-refs are bound at proper timing (2)", async () => {
expect.assertions(10);
class Child extends Component {
static template = xml`<div>widget</div>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child t-if="state.child1" t-ref="child1"/>
<Child t-if="state.child2" t-ref="child2"/>
</div>`;
static components = { Child };
state = useState({ child1: true, child2: false });
child1 = useRef("child1");
child2 = useRef("child2");
count = 0;
mounted() {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
willPatch() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeNull();
}
if (this.count === 1) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
}
patched() {
if (this.count === 0) {
expect(this.child1.comp).toBeDefined();
expect(this.child2.comp).toBeDefined();
}
if (this.count === 1) {
expect(this.child1.comp).toBeNull();
expect(this.child2.comp).toBeDefined();
}
this.count++;
}
}
const parent = await mount(Parent, fixture);
parent.state.child2 = true;
await nextTick();
parent.state.child1 = false;
await nextTick();
});
});
@@ -201,6 +201,41 @@ exports[`Portal portal could have dynamically no content 1`] = `
}"
`;
exports[`Portal portal destroys on crash 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['props'].error&&this.will.crash;
return block1([d1]);
}
}"
`;
exports[`Portal portal destroys on crash 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => {
return component(\`Child\`, {error: ctx['state'].error}, key + \`__3\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
`;
exports[`Portal portal with child and props 1`] = `
"function anonymous(bdom, helpers
) {
+170
View File
@@ -0,0 +1,170 @@
//import { AsyncRoot } from "../../src/misc/async_root";
import { useState, mount } from "../../src";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestFixture, snapshotEverything, nextTick } from "../helpers";
import { Component } from "../../src/component/component";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("Asyncroot", () => {
test.skip("delayed component with AsyncRoot component", async () => {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
static components = { Child, AsyncChild, /*AsyncRoot*/ };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
await mount(Parent, fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
// click on button to increment Parent counter
const def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test.skip("fast component with AsyncRoot", async () => {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<AsyncRoot>
<Child val="state.val"/>
</AsyncRoot>
<AsyncChild val="state.val"/>
</div>
</div>`;
static components = { Child, AsyncChild, /*AsyncRoot*/ };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
await mount(Parent, fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0</span><span>0</span>");
// click on button to increment Parent counter
const def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test.skip("asyncroot component: mixed re-renderings", async () => {
class Child extends Component {
static template = xml`
<span t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>`;
state = useState({ val: 0 });
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
static components = { Child, AsyncChild, /*AsyncRoot*/ };
state = useState({ val: 0 });
updateApp() {
this.state.val++;
}
}
const parent = await mount(Parent, fixture);
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/0</span><span>0/0</span>");
// click on button to increment Parent counter
const def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/1</span><span>0/0</span>");
// click on each Child to increment their local counter
const children = (<HTMLElement>parent.el).querySelectorAll("span");
children[0]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>0/0</span>");
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
});
});
+70 -276
View File
@@ -379,283 +379,35 @@ describe("Portal", () => {
]);
});
// test.skip("portal destroys on crash", async () => {
// class Child extends Component {
// static template = xml`<span t-esc="props.error and this.will.crash" />`;
// state = {};
// }
// class Parent extends Component {
// static components = { Portal, Child };
// static template = xml`
// <div>
// <Portal target="'#outside'" >
// <Child error="state.error"/>
// </Portal>
// </div>`;
// state = { error: false };
// }
test("portal destroys on crash", async () => {
class Child extends Component {
static template = xml`<span t-esc="props.error and this.will.crash" />`;
state = {};
}
class Parent extends Component {
static components = { Portal, Child };
static template = xml`
<div>
<Portal target="'#outside'" >
<Child error="state.error"/>
</Portal>
</div>`;
state = { error: false };
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
parent.state.error = true;
// const parent = new Parent();
// await parent.mount(fixture);
// parent.state.error = true;
// let error;
// try {
// await parent.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);
// });
// test.skip("portal manual unmount", async () => {
// class Parent extends Component {
// static components = { Portal };
// static template = xml`
// <div>
// <Portal target="'#outside'">
// <span>gloria</span>
// </Portal>
// </div>`;
// }
// const parent = new Parent();
// await parent.mount(fixture);
// expect(outside.innerHTML).toBe("<span>gloria</span>");
// expect(parent.el!.innerHTML).toBe("<portal></portal>");
// parent.unmount();
// expect(outside.innerHTML).toBe("");
// expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
// await parent.mount(fixture);
// expect(outside.innerHTML).toBe("<span>gloria</span>");
// expect(parent.el!.innerHTML).toBe("<portal></portal>");
// });
// test.skip("portal manual unmount with subcomponent", async () => {
// expect.assertions(9);
// class Child extends Component {
// static template = xml`<span>gloria</span>`;
// mounted() {
// expect(outside.contains(this.el)).toBeTruthy();
// }
// willUnmount() {
// expect(outside.contains(this.el)).toBeTruthy();
// }
// }
// class Parent extends Component {
// static components = { Portal, Child };
// static template = xml`
// <div>
// <Portal target="'#outside'">
// <Child />
// </Portal>
// </div>`;
// }
// const parent = new Parent();
// await parent.mount(fixture);
// expect(outside.innerHTML).toBe("<span>gloria</span>");
// expect(parent.el!.innerHTML).toBe("<portal></portal>");
// parent.unmount();
// expect(outside.innerHTML).toBe("");
// expect(parent.el!.innerHTML).toBe("<portal><span>gloria</span></portal>");
// await parent.mount(fixture);
// expect(outside.innerHTML).toBe("<span>gloria</span>");
// expect(parent.el!.innerHTML).toBe("<portal></portal>");
// });
// });
// describe("Portal: Events handling", () => {
// test.skip("events triggered on movable pure node are handled", async () => {
// class Parent extends Component {
// static components = { Portal };
// static template = xml`
// <div>
// <Portal target="'#outside'">
// <span id="trigger-me" t-on-custom="_onCustom" t-esc="state.val"/>
// </Portal>
// </div>`;
// state = useState({ val: "ab" });
// _onCustom() {
// this.state.val = "triggered";
// }
// }
// const parent = new Parent();
// await parent.mount(fixture);
// expect(outside.innerHTML).toBe(`<span id="trigger-me">ab</span>`);
// outside.querySelector("#trigger-me")!.dispatchEvent(new Event("custom"));
// await nextTick();
// expect(outside.innerHTML).toBe(`<span id="trigger-me">triggered</span>`);
// });
// test.skip("events triggered on movable owl components are redirected", async () => {
// let childInst: Component | null = null;
// class Child extends Component {
// static template = xml`
// <span t-on-custom="_onCustom" t-esc="props.val"/>`;
// constructor(parent, props) {
// super(parent, props);
// childInst = this;
// }
// _onCustom() {
// this.trigger("custom-portal");
// }
// }
// class Parent extends Component {
// static components = { Portal, Child };
// static template = xml`
// <div t-on-custom-portal="_onCustomPortal">
// <Portal target="'#outside'">
// <Child val="state.val"/>
// </Portal>
// </div>`;
// state = useState({ val: "ab" });
// _onCustomPortal() {
// this.state.val = "triggered";
// }
// }
// const parent = new Parent();
// await parent.mount(fixture);
// expect(outside.innerHTML).toBe(`<span>ab</span>`);
// childInst!.trigger("custom");
// await nextTick();
// expect(outside.innerHTML).toBe(`<span>triggered</span>`);
// });
// test.skip("events triggered on contained movable owl components are redirected", async () => {
// const steps: string[] = [];
// let childInst: Component | null = null;
// class Child extends Component {
// static template = xml`
// <span t-on-custom="_onCustom"/>`;
// constructor(parent, props) {
// super(parent, props);
// childInst = this;
// }
// _onCustom() {
// this.trigger("custom-portal");
// }
// }
// class Parent extends Component {
// static components = { Portal, Child };
// static template = xml`
// <div t-on-custom="_handled" t-on-custom-portal="_handled">
// <Portal target="'#outside'">
// <div>
// <Child/>
// </div>
// </Portal>
// </div>`;
// _handled(ev) {
// steps.push(ev.type);
// }
// }
// const parent = new Parent();
// await parent.mount(fixture);
// childInst!.trigger("custom");
// await nextTick();
// // This is expected because trigger is synchronous
// expect(steps).toMatchObject(["custom-portal", "custom"]);
// });
// test.skip("Dom events are not mapped", async () => {
// let childInst: Component | null = null;
// const steps: string[] = [];
// class Child extends Component {
// static template = xml`
// <button>child</button>`;
// constructor(parent, props) {
// super(parent, props);
// childInst = this;
// }
// }
// class Parent extends Component {
// static components = { Portal, Child };
// static template = xml`
// <div t-on-click="_handled">
// <Portal target="'#outside'">
// <Child />
// </Portal>
// </div>`;
// _handled(ev) {
// steps.push(ev.type as string);
// }
// }
// const bodyListener = (ev) => {
// steps.push(`body: ${ev.type}`);
// };
// document.body.addEventListener("click", bodyListener);
// const parent = new Parent();
// await parent.mount(fixture);
// childInst!.el!.click();
// expect(steps).toEqual(["body: click"]);
// document.body.removeEventListener("click", bodyListener);
// });
// test.skip("Nested portals event propagation", async () => {
// const outside2 = document.createElement("div");
// outside2.setAttribute("id", "outside2");
// fixture.appendChild(outside2);
// const steps: Array<string> = [];
// let childInst: Component | null = null;
// class Child2 extends Component {
// static template = xml`<div>child2</div>`;
// constructor(parent, props) {
// super(parent, props);
// childInst = this;
// }
// }
// class Child extends Component {
// static components = { Portal, Child2 };
// static template = xml`
// <Portal target="'#outside2'">
// <Child2 />
// </Portal>`;
// }
// class Parent extends Component {
// static components = { Portal, Child };
// static template = xml`
// <div t-on-custom='_handled'>
// <Portal target="'#outside'">
// <Child/>
// </Portal>
// </div>`;
// _handled(ev) {
// steps.push(`${ev.type} from ${ev.originalComponent.constructor.name}`);
// }
// }
// const parent = new Parent();
// await parent.mount(fixture);
// childInst!.trigger("custom");
// expect(steps).toEqual(["custom from Child2"]);
// });
let error;
try {
await parent.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);
});
test("portal's parent's env is not polluted", async () => {
class Child extends Component {
@@ -750,3 +502,45 @@ describe("Portal: UI/UX", () => {
expect(document.activeElement === inputReRendered).toBeTruthy();
});
});
describe("Portal: Props validation", () => {
test.skip("target is mandatory", async () => {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
<Portal>
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`);
});
test.skip("target is not list", async () => {
class Parent extends Component {
static components = { Portal };
static template = xml`
<div>
<Portal target="['body']">
<div>2</div>
</Portal>
</div>`;
}
let error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
});
});
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`error handling cannot add twice the same template 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
@@ -178,6 +178,45 @@ exports[`simple templates, mostly static empty string in a template set 1`] = `
}"
`;
exports[`simple templates, mostly static inline template string in t-esc 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(\`text\`);
}
}"
`;
exports[`simple templates, mostly static inline template string with content in t-esc 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"v\\", 1);
return text(\`text\${ctx['v']}\`);
}
}"
`;
exports[`simple templates, mostly static inline template string with variable in context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(\`text \${ctx['v']}\`);
}
}"
`;
exports[`simple templates, mostly static multiple root nodes 1`] = `
"function anonymous(bdom, helpers
) {
+40
View File
@@ -0,0 +1,40 @@
import { renderToString, snapshotEverything, TestContext } from "../helpers";
snapshotEverything();
describe("error handling", () => {
test("invalid xml", () => {
expect(() => renderToString("<div>")).toThrow("Invalid XML in template");
});
test("nice warning if no template with given name", () => {
const context = new TestContext();
expect(() => context.renderToString("invalidname")).toThrow("Missing template");
});
test("cannot add twice the same template", () => {
const context = new TestContext();
context.addTemplate("test", `<t></t>`);
expect(() => context.addTemplate("test", "<div/>", { allowDuplicate: true })).not.toThrow("already defined");
expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined");
});
// test("addTemplates throw if parser error", () => {
// const context = new TestContext();
// expect(() => {
// context.addTemplates("<templates><abc>></templates>");
// }).toThrow("Invalid XML in template");
// });
test("nice error when t-on is evaluated with a missing event", () => {
expect(() => renderToString(`<div t-on="somemethod"></div>`)).toThrow(
"Missing event name with t-on directive"
);
});
test("error when unknown directive", () => {
expect(() => renderToString(`<div t-best-beer="rochefort 10">test</div>`)).toThrow(
"Unknown QWeb directive: 't-best-beer'"
);
});
});
+15
View File
@@ -122,6 +122,21 @@ describe("simple templates, mostly static", () => {
const template = `<tr><td>cell</td></tr>`;
expect(renderToString(template)).toBe(template);
});
test("inline template string in t-esc", () => {
const template = '<t><t t-esc="`text`"/></t>';
expect(renderToString(template)).toBe("text");
});
test("inline template string with content in t-esc", () => {
const template = '<t><t t-set="v" t-value="1"/><t t-esc="`text${v}`"/></t>';
expect(renderToString(template)).toBe("text1");
});
test("inline template string with variable in context", () => {
const template = '<t><t t-esc="`text ${v}`"/></t>';
expect(renderToString(template, { v: "from context" })).toBe("text from context");
});
});
describe("loading templates", () => {
+31
View File
@@ -0,0 +1,31 @@
import {
renderToString,
snapshotEverything,
} from "../helpers";
snapshotEverything();
describe("qweb t-tag", () => {
test.skip("simple usecases", () => {
expect(renderToString(`<t t-tag="'div'"></t>`)).toBe("<div></div>");
expect(renderToString(`<t t-tag="tag">text</t>`, { tag: "span" })).toBe("<span>text</span>");
});
test.skip("with multiple child nodes", () => {
const template = `
<t t-tag="tag">
pear
<span>apple</span>
strawberry
</t>`;
expect(renderToString(template, { tag: "div" })).toBe(
"<div> pear <span>apple</span> strawberry </div>"
);
});
test.skip("with multiple attributes", () => {
const template = `<t t-tag="tag" class="blueberry" taste="raspberry">gooseberry</t>`;
const expected = `<div taste=\"raspberry\" class=\"blueberry\">gooseberry</div>`;
expect(renderToString(template, { tag: "div" })).toBe(expected);
});
});