From e01fe301c0acb73b9a50479e82f695682eef0bd1 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 21 Oct 2021 10:13:08 +0200 Subject: [PATCH] [IMP] *: re-add a bunch of tests --- src/component/lifecycle_hooks.ts | 4 +- src/refs.ts | 2 +- .../__snapshots__/basics.test.ts.snap | 126 +++++++ .../__snapshots__/event_handling.test.ts.snap | 25 ++ .../__snapshots__/hooks.test.ts.snap | 128 +++++++ .../__snapshots__/lifecycle.test.ts.snap | 93 +++++ tests/components/basics.test.ts | 217 +++++++++++ tests/components/event_handling.test.ts | 20 + tests/components/hooks.test.ts | 303 +++++++++++++++ tests/components/lifecycle.test.ts | 95 +++++ tests/components/refs.test.ts | 100 +++++ tests/misc/__snapshots__/portal.test.ts.snap | 35 ++ tests/misc/async_root.test.ts | 170 +++++++++ tests/misc/portal.test.ts | 346 ++++-------------- .../__snapshots__/error_handling.test.ts.snap | 15 + .../simple_templates.test.ts.snap | 39 ++ tests/qweb/error_handling.test.ts | 40 ++ tests/qweb/simple_templates.test.ts | 15 + tests/qweb/t_tag.test.ts | 31 ++ 19 files changed, 1525 insertions(+), 279 deletions(-) create mode 100644 tests/components/__snapshots__/hooks.test.ts.snap create mode 100644 tests/components/hooks.test.ts create mode 100644 tests/misc/async_root.test.ts create mode 100644 tests/qweb/__snapshots__/error_handling.test.ts.snap create mode 100644 tests/qweb/error_handling.test.ts create mode 100644 tests/qweb/t_tag.test.ts diff --git a/src/component/lifecycle_hooks.ts b/src/component/lifecycle_hooks.ts index 18497347..fbd9adff 100644 --- a/src/component/lifecycle_hooks.ts +++ b/src/component/lifecycle_hooks.ts @@ -16,12 +16,12 @@ export function onWillUpdateProps(fn: (nextProps: any) => Promise | void | export function onMounted(fn: () => void | any) { const node = getCurrent()!; - node.mounted.push(fn); + node.mounted.unshift(fn); } export function onWillPatch(fn: () => Promise | any | void) { const node = getCurrent()!; - node.willPatch.push(fn); + node.willPatch.unshift(fn); } export function onPatched(fn: () => void | any) { diff --git a/src/refs.ts b/src/refs.ts index 2c3ef93a..705d47b9 100644 --- a/src/refs.ts +++ b/src/refs.ts @@ -19,7 +19,7 @@ export function useRef(name: string): Ref { 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) { diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap index 4744b613..ca0b2c8b 100644 --- a/tests/components/__snapshots__/basics.test.ts.snap +++ b/tests/components/__snapshots__/basics.test.ts.snap @@ -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(\`\`); + + 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(\`abc\`); + + 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(\`\`); + + 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(\`\`); + + 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(\`
\`); + + 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(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = html(ctx['state'].value); + return block1([], [b2]); + } +}" +`; diff --git a/tests/components/__snapshots__/event_handling.test.ts.snap b/tests/components/__snapshots__/event_handling.test.ts.snap index c23d9860..68f3de1a 100644 --- a/tests/components/__snapshots__/event_handling.test.ts.snap +++ b/tests/components/__snapshots__/event_handling.test.ts.snap @@ -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(\`
\`); + let block3 = createBlock(\`
\`); + + 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]); + } +}" +`; diff --git a/tests/components/__snapshots__/hooks.test.ts.snap b/tests/components/__snapshots__/hooks.test.ts.snap new file mode 100644 index 00000000..eab8710f --- /dev/null +++ b/tests/components/__snapshots__/hooks.test.ts.snap @@ -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(\`
\`); + let block2 = createBlock(\`\`); + + 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(\`
\`); + + 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(\`\`); + + 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(\`
\`); + + 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(\`
hey
\`); + + 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(\`
hey
\`); + + 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(\`
\`); + + 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]); + } +}" +`; diff --git a/tests/components/__snapshots__/lifecycle.test.ts.snap b/tests/components/__snapshots__/lifecycle.test.ts.snap index ec6b0f90..603e9c53 100644 --- a/tests/components/__snapshots__/lifecycle.test.ts.snap +++ b/tests/components/__snapshots__/lifecycle.test.ts.snap @@ -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(\`
B
\`); + + 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(\`
D
\`); + + 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(\`
E
\`); + + 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(\`
F
\`); + + 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(\`
C
\`); + + 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(\`
A
\`); + + 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 ) { diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts index 38593863..e200d8fb 100644 --- a/tests/components/basics.test.ts +++ b/tests/components/basics.test.ts @@ -741,6 +741,165 @@ describe("basics", () => { await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
asdfasdf
"); }); + // 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`abcdef`; + } + class Parent extends Component { + static template = xml`` + static components = { Child }; + state = useState({ flag: false }); + } + + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("abc"); + parent.state.flag = true; + await nextTick(); + expect(fixture.innerHTML).toBe("abcdef"); + }); + + test("can inject values in tagged templates", async () => { + const SUBTEMPLATE = xml``; + class Parent extends Component { + static template = xml``; + state = useState({ n: 42 }); + } + + await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("42"); + }); + // Depends on t-props + test.skip("update props of component without concrete own node", async () => { + class Custom extends Component { + static template = xml` +
+ __ +
`; + } + class Child extends Component { + static components = { Custom }; + static template = xml` + `; + } + + class Parent extends Component { + static components = { Child }; + static template = xml` +
+ +
`; + 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`
`; + setup() { + this.props.obj.coffee = 2; + } + } + class Parent extends Component { + static template = xml`
`; + 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` + + + + `; + setup() { + expect(this.props).toEqual({ a: 1, b: 2 }); + expect(this.props).not.toBe(parent.some.obj); + } + } + class Parent extends Component { + static template = xml` +
+ +
+ `; + static components = { Child }; + + some = { obj: { a: 1, b: 2 } }; + } + + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
3
"); + }); + + test.skip("t-props with props", async () => { + expect.assertions(1); + + class Child extends Component { + static template = xml`
`; + setup() { + expect(this.props).toEqual({ a: 1, b: 2, c: "c" }); + } + } + class Parent extends Component { + static template = xml` +
+ +
+ `; + 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` + + + `; + } + + class Svg extends Component { + static template = xml` + + + `; + static components = { GComp }; + } + await mount(Svg, fixture); + + expect(fixture.innerHTML).toBe( + '' + ); + }); +}); + +describe("t-raw in components", () => { + test("update properly on state changes", async () => { + class Test extends Component { + static template = xml`
`; + state = useState({ value: "content" }); + } + const component = await mount(Test, fixture); + + expect(fixture.innerHTML).toBe("
content
"); + + component.state.value = "other content"; + await nextTick(); + expect(fixture.innerHTML).toBe("
other content
"); + }); + + test("can render list of t-raw ", async () => { + class Test extends Component { + static template = xml` +
+ + + + +
`; + state = useState({ items: ["one", "two", "tree"] }); + } + await mount(Test, fixture); + + expect(fixture.innerHTML).toBe( + "
<b>one</b>one<b>two</b>two<b>tree</b>tree
" + ); + }); +}); diff --git a/tests/components/event_handling.test.ts b/tests/components/event_handling.test.ts index 9cf7face..63cfb7c3 100644 --- a/tests/components/event_handling.test.ts +++ b/tests/components/event_handling.test.ts @@ -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` +
+ +
+ +
`; + items = [1, 2, 3, 4]; + onClick(n: number, ev: MouseEvent) { + expect(n).toBe(1); + expect(ev).toBeInstanceOf(MouseEvent); + } + } + + await mount(Parent, fixture); + (fixture.querySelector('.item')).click(); + }); }); diff --git a/tests/components/hooks.test.ts b/tests/components/hooks.test.ts new file mode 100644 index 00000000..b01ee886 --- /dev/null +++ b/tests/components/hooks.test.ts @@ -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`
`; + 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("
"); + expect(counter!.button.el).not.toBe(null); + expect(counter!.button.el).toBe(fixture.querySelector("button")); + counter!.increment(); + expect(fixture.innerHTML).toBe("
"); + }); + // 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`
hey
`; + state = useState({ value: 1 }); + setup() { + useMyHook(1); + useMyHook(2); + } + } + const component = await mount(Test, fixture); + expect(fixture.innerHTML).toBe("
hey1
"); + component.state.value++; + await nextTick(); + expect(fixture.innerHTML).toBe("
hey2
"); + + 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`
hey
`; + 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` +
+ + +
`; + + setup() { + useAutofocus("input2"); + } + } + + await mount(Test, fixture); + expect(fixture.innerHTML).toBe("
"); + 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` +
+ + +
`; + + state = useState({ flag: false }); + setup() { + useAutofocus("input2"); + } + } + + const component = await mount(Test, fixture); + expect(fixture.innerHTML).toBe("
"); + 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`
`; + setup() { + //expect(useEnv()).toBe(this.env); + } + } + await mount(Test, fixture); + }); + + test.skip("can use sub env", async () => { + class Test extends Component { + static template = xml`
`; + setup() { + //useSubEnv({ val: 3 }); + } + } + const component = await mount(Test, fixture); + expect(fixture.innerHTML).toBe("
3
"); + 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`
`; + setup() { + expect(useComponent()).toBe(this); + } + } + await mount(Test, fixture); + }); + + test.skip("parent and child env", async () => { + class Child extends Component { + static template = xml`
`; + super() { + //useSubEnv({ val: 5 }); + } + } + + class Parent extends Component { + static template = xml``; + static components = { Child }; + setup() { + //useSubEnv({ val: 3 }); + } + } + mount(Parent, fixture); + expect(fixture.innerHTML).toBe("3
5
"); + }); + + test("can use onWillStart, onWillUpdateProps", async () => { + const steps: string[] = []; + async function slow(): Promise { + 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``; + setup() { + useMyHook(); + use2ndHook(); + } + } + class App extends Component { + static template = xml``; + static components = { MyComponent }; + state = useState({ value: 1 }); + } + + const app = await mount(App, fixture); + expect(fixture.innerHTML).toBe("1"); + + // 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("2"); + expect(steps).toEqual([ + "on2ndStart", + "slow", + "onWillStart", + "on2ndUpdate", + "slow", + "onWillUpdateProps", + ]); + }); + + test.skip("useExternalListener", async () => { + let n = 0; + + class MyComponent extends Component { + static template = xml``; + setup() { + //useExternalListener(window as any, "click", this.increment); + } + increment() { + n++; + } + } + class App extends Component { + static template = xml``; + 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); + }); +}); diff --git a/tests/components/lifecycle.test.ts b/tests/components/lifecycle.test.ts index bdce15c3..f570caf8 100644 --- a/tests/components/lifecycle.test.ts +++ b/tests/components/lifecycle.test.ts @@ -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`
B
`; + name = "B"; + } + class D extends TestWidget { + static template = xml`
D
`; + name = "D"; + } + class E extends TestWidget { + static template = xml`
E
`; + name = "E"; + } + + class F extends TestWidget { + static template = xml`
F
`; + name = "F"; + } + let c: C; + class C extends TestWidget { + static template = xml` +
C + + +
`; + static components = { D, E, F }; + name = "C"; + state = useState({ flag: true }); + + setup() { + c = this; + super.setup(); + } + } + class A extends TestWidget { + static template = xml`
A
`; + static components = { B, C }; + name = "A"; + } + + await mount(A, fixture); + expect(fixture.innerHTML).toBe(`
A
B
C
D
E
`); + 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", + ]); + }); }); diff --git a/tests/components/refs.test.ts b/tests/components/refs.test.ts index 4693541d..03fa8506 100644 --- a/tests/components/refs.test.ts +++ b/tests/components/refs.test.ts @@ -44,4 +44,104 @@ describe("refs", () => { '
1
' ); }); + // TODO: rename + test.skip("t-refs on widget are components", async () => { + class Child extends Component { + static template = xml`
b
`; + } + let parent: Parent; + class Parent extends Component { + static template = xml`
Hello
`; + static components = { Child }; + ref = useRef("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`
widget
`; + } + + class Parent extends Component { + static template = xml` +
+ +
+ `; + static components = { Child }; + state = useState({ list: [] }); + 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`
widget
`; + } + class Parent extends Component { + static template = xml` +
+ + +
`; + 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(); + }); }); diff --git a/tests/misc/__snapshots__/portal.test.ts.snap b/tests/misc/__snapshots__/portal.test.ts.snap index 5a4c9a16..a2f829b2 100644 --- a/tests/misc/__snapshots__/portal.test.ts.snap +++ b/tests/misc/__snapshots__/portal.test.ts.snap @@ -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(\`\`); + + 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(\`
\`); + + 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 ) { diff --git a/tests/misc/async_root.test.ts b/tests/misc/async_root.test.ts new file mode 100644 index 00000000..5e43a1af --- /dev/null +++ b/tests/misc/async_root.test.ts @@ -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``; + } + class AsyncChild extends Child { + willUpdateProps() { + return def; + } + } + class Parent extends Component { + static template = xml` +
+ +
+ + + + +
+
`; + static components = { Child, AsyncChild, /*AsyncRoot*/ }; + state = useState({ val: 0 }); + + updateApp() { + this.state.val++; + } + } + + await mount(Parent, fixture); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); + + // click on button to increment Parent counter + const def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); + + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); + }); + + test.skip("fast component with AsyncRoot", async () => { + class Child extends Component { + static template = xml``; + } + class AsyncChild extends Child { + willUpdateProps() { + return def; + } + } + + class Parent extends Component { + static template = xml` +
+ +
+ + + + +
+
`; + static components = { Child, AsyncChild, /*AsyncRoot*/ }; + state = useState({ val: 0 }); + + updateApp() { + this.state.val++; + } + } + + await mount(Parent, fixture); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); + + // click on button to increment Parent counter + const def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); + + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); + }); + + test.skip("asyncroot component: mixed re-renderings", async () => { + class Child extends Component { + static template = xml` + + / + `; + state = useState({ val: 0 }); + + increment() { + this.state.val++; + } + } + class AsyncChild extends Child { + willUpdateProps() { + return def; + } + } + class Parent extends Component { + static template = xml` +
+ +
+ + + + +
+
`; + 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("0/00/0"); + + // click on button to increment Parent counter + const def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("0/10/0"); + + // click on each Child to increment their local counter + const children = (parent.el).querySelectorAll("span"); + children[0]!.click(); + await nextTick(); + expect(fixture.querySelector(".children")!.innerHTML).toBe("1/10/0"); + + children[1]!.click(); + await nextTick(); + expect(fixture.querySelector(".children")!.innerHTML).toBe("1/10/0"); + + // finalize first re-rendering (coming from the props update) + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/1"); + }); +}); diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index 04526f75..bb346b46 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -379,283 +379,35 @@ describe("Portal", () => { ]); }); - // test.skip("portal destroys on crash", async () => { - // class Child extends Component { - // static template = xml``; - // state = {}; - // } - // class Parent extends Component { - // static components = { Portal, Child }; - // static template = xml` - //
- // - // - // - //
`; - // state = { error: false }; - // } + test("portal destroys on crash", async () => { + class Child extends Component { + static template = xml``; + state = {}; + } + class Parent extends Component { + static components = { Portal, Child }; + static template = xml` +
+ + + +
`; + 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` - //
- // - // gloria - // - //
`; - // } - - // const parent = new Parent(); - // await parent.mount(fixture); - - // expect(outside.innerHTML).toBe("gloria"); - // expect(parent.el!.innerHTML).toBe(""); - - // parent.unmount(); - // expect(outside.innerHTML).toBe(""); - // expect(parent.el!.innerHTML).toBe("gloria"); - - // await parent.mount(fixture); - // expect(outside.innerHTML).toBe("gloria"); - // expect(parent.el!.innerHTML).toBe(""); - // }); - - // test.skip("portal manual unmount with subcomponent", async () => { - // expect.assertions(9); - // class Child extends Component { - // static template = xml`gloria`; - // 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` - //
- // - // - // - //
`; - // } - - // const parent = new Parent(); - // await parent.mount(fixture); - - // expect(outside.innerHTML).toBe("gloria"); - // expect(parent.el!.innerHTML).toBe(""); - - // parent.unmount(); - // expect(outside.innerHTML).toBe(""); - // expect(parent.el!.innerHTML).toBe("gloria"); - - // await parent.mount(fixture); - // expect(outside.innerHTML).toBe("gloria"); - // expect(parent.el!.innerHTML).toBe(""); - // }); - // }); - - // 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` - //
- // - // - // - //
`; - // state = useState({ val: "ab" }); - - // _onCustom() { - // this.state.val = "triggered"; - // } - // } - // const parent = new Parent(); - // await parent.mount(fixture); - - // expect(outside.innerHTML).toBe(`ab`); - // outside.querySelector("#trigger-me")!.dispatchEvent(new Event("custom")); - // await nextTick(); - // expect(outside.innerHTML).toBe(`triggered`); - // }); - - // test.skip("events triggered on movable owl components are redirected", async () => { - // let childInst: Component | null = null; - // class Child extends Component { - // static template = xml` - // `; - - // constructor(parent, props) { - // super(parent, props); - // childInst = this; - // } - - // _onCustom() { - // this.trigger("custom-portal"); - // } - // } - // class Parent extends Component { - // static components = { Portal, Child }; - // static template = xml` - //
- // - // - // - //
`; - // state = useState({ val: "ab" }); - - // _onCustomPortal() { - // this.state.val = "triggered"; - // } - // } - // const parent = new Parent(); - // await parent.mount(fixture); - - // expect(outside.innerHTML).toBe(`ab`); - // childInst!.trigger("custom"); - // await nextTick(); - // expect(outside.innerHTML).toBe(`triggered`); - // }); - - // 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` - // `; - - // constructor(parent, props) { - // super(parent, props); - // childInst = this; - // } - - // _onCustom() { - // this.trigger("custom-portal"); - // } - // } - // class Parent extends Component { - // static components = { Portal, Child }; - // static template = xml` - //
- // - //
- // - //
- //
- //
`; - - // _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` - // `; - - // constructor(parent, props) { - // super(parent, props); - // childInst = this; - // } - // } - // class Parent extends Component { - // static components = { Portal, Child }; - // static template = xml` - //
- // - // - // - //
`; - - // _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 = []; - // let childInst: Component | null = null; - // class Child2 extends Component { - // static template = xml`
child2
`; - // constructor(parent, props) { - // super(parent, props); - // childInst = this; - // } - // } - // class Child extends Component { - // static components = { Portal, Child2 }; - // static template = xml` - // - // - // `; - // } - // class Parent extends Component { - // static components = { Portal, Child }; - // static template = xml` - //
- // - // - // - //
`; - - // _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` +
+ +
2
+
+
`; + } + 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` +
+ +
2
+
+
`; + } + let error; + try { + await mount(Parent, fixture); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`); + }); +}); diff --git a/tests/qweb/__snapshots__/error_handling.test.ts.snap b/tests/qweb/__snapshots__/error_handling.test.ts.snap new file mode 100644 index 00000000..f364a41c --- /dev/null +++ b/tests/qweb/__snapshots__/error_handling.test.ts.snap @@ -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(\`
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; diff --git a/tests/qweb/__snapshots__/simple_templates.test.ts.snap b/tests/qweb/__snapshots__/simple_templates.test.ts.snap index 42677945..9f2f2f8f 100644 --- a/tests/qweb/__snapshots__/simple_templates.test.ts.snap +++ b/tests/qweb/__snapshots__/simple_templates.test.ts.snap @@ -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 ) { diff --git a/tests/qweb/error_handling.test.ts b/tests/qweb/error_handling.test.ts new file mode 100644 index 00000000..461145bb --- /dev/null +++ b/tests/qweb/error_handling.test.ts @@ -0,0 +1,40 @@ +import { renderToString, snapshotEverything, TestContext } from "../helpers"; + +snapshotEverything(); + +describe("error handling", () => { + test("invalid xml", () => { + expect(() => renderToString("
")).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", ``); + expect(() => context.addTemplate("test", "
", { allowDuplicate: true })).not.toThrow("already defined"); + expect(() => context.addTemplate("test", "
")).toThrow("already defined"); + }); + + // test("addTemplates throw if parser error", () => { + // const context = new TestContext(); + // expect(() => { + // context.addTemplates(">"); + // }).toThrow("Invalid XML in template"); + // }); + + test("nice error when t-on is evaluated with a missing event", () => { + expect(() => renderToString(`
`)).toThrow( + "Missing event name with t-on directive" + ); + }); + + test("error when unknown directive", () => { + expect(() => renderToString(`
test
`)).toThrow( + "Unknown QWeb directive: 't-best-beer'" + ); + }); +}); \ No newline at end of file diff --git a/tests/qweb/simple_templates.test.ts b/tests/qweb/simple_templates.test.ts index dcbf9b68..a431adaf 100644 --- a/tests/qweb/simple_templates.test.ts +++ b/tests/qweb/simple_templates.test.ts @@ -122,6 +122,21 @@ describe("simple templates, mostly static", () => { const template = `cell`; expect(renderToString(template)).toBe(template); }); + + test("inline template string in t-esc", () => { + const template = ''; + expect(renderToString(template)).toBe("text"); + }); + + test("inline template string with content in t-esc", () => { + const template = ''; + expect(renderToString(template)).toBe("text1"); + }); + + test("inline template string with variable in context", () => { + const template = ''; + expect(renderToString(template, { v: "from context" })).toBe("text from context"); + }); }); describe("loading templates", () => { diff --git a/tests/qweb/t_tag.test.ts b/tests/qweb/t_tag.test.ts new file mode 100644 index 00000000..258a8f73 --- /dev/null +++ b/tests/qweb/t_tag.test.ts @@ -0,0 +1,31 @@ +import { + renderToString, + snapshotEverything, +} from "../helpers"; + +snapshotEverything(); + +describe("qweb t-tag", () => { + test.skip("simple usecases", () => { + expect(renderToString(``)).toBe("
"); + expect(renderToString(`text`, { tag: "span" })).toBe("text"); + }); + + test.skip("with multiple child nodes", () => { + const template = ` + + pear + apple + strawberry + `; + expect(renderToString(template, { tag: "div" })).toBe( + "
pear apple strawberry
" + ); + }); + + test.skip("with multiple attributes", () => { + const template = `gooseberry`; + const expected = `
gooseberry
`; + expect(renderToString(template, { tag: "div" })).toBe(expected); + }); +}); \ No newline at end of file