mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79a8139697 | |||
| 73c339fff1 | |||
| 406be446a5 | |||
| bd2aa8a72f | |||
| 3536f41f00 | |||
| 804ad3c35e |
@@ -1,25 +0,0 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.core.EventBus();
|
||||
|
||||
bus.on("some-event", null, function (...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
+7
-3
@@ -18,10 +18,14 @@ export interface AppConfig<P, E> extends TemplateSetConfig {
|
||||
test?: boolean;
|
||||
}
|
||||
|
||||
export const DEV_MSG = `Owl is running in 'dev' mode.
|
||||
export const DEV_MSG = () => {
|
||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
||||
|
||||
return `Owl is running in 'dev' mode.
|
||||
|
||||
This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
|
||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
||||
};
|
||||
|
||||
export class App<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
@@ -43,7 +47,7 @@ export class App<
|
||||
this.dev = true;
|
||||
}
|
||||
if (this.dev && !config.test) {
|
||||
console.info(DEV_MSG);
|
||||
console.info(DEV_MSG());
|
||||
}
|
||||
const descrs = Object.getOwnPropertyDescriptors(config.env || {});
|
||||
this.env = Object.freeze(Object.defineProperties({}, descrs)) as E;
|
||||
|
||||
@@ -139,7 +139,7 @@ export function updateClass(this: HTMLElement, val: any, oldVal: any) {
|
||||
|
||||
export function makePropSetter(name: string): Setter<HTMLElement> {
|
||||
return function setProp(this: HTMLElement, value: any) {
|
||||
(this as any)[name] = value;
|
||||
(this as any)[name] = value || "";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -525,6 +525,54 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
this.parentEl = parent;
|
||||
}
|
||||
patch(other: Block, withBeforeRemove: boolean) {}
|
||||
|
||||
hydrate(parent: HTMLElement, el: HTMLElement) {
|
||||
this.parentEl = parent;
|
||||
this.el = el;
|
||||
const refs: Node[] = new Array(refN);
|
||||
this.refs = refs;
|
||||
refs[0] = el;
|
||||
for (let i = 0; i < colN; i++) {
|
||||
const w = collectors[i];
|
||||
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
|
||||
}
|
||||
|
||||
// applying data to all update points
|
||||
if (locN) {
|
||||
const data = this.data!;
|
||||
for (let i = 0; i < locN; i++) {
|
||||
const loc = locations[i];
|
||||
loc.setData.call(refs[loc.refIdx], data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// preparing all children
|
||||
if (childN) {
|
||||
const children = this.children;
|
||||
for (let i = 0; i < childN; i++) {
|
||||
const child = children![i];
|
||||
if (child) {
|
||||
const loc = childrenLocs[i];
|
||||
let target: HTMLElement;
|
||||
if (loc.afterRefIdx) {
|
||||
target = refs[loc.afterRefIdx] as HTMLElement;
|
||||
const afterNode = document.createTextNode("");
|
||||
target.parentElement!.insertBefore(afterNode, target.nextSibling);
|
||||
refs[loc.afterRefIdx!] = afterNode;
|
||||
} else {
|
||||
target = refs[loc.parentRefIdx].firstChild! as HTMLElement;
|
||||
}
|
||||
|
||||
// const target = (loc.afterRefIdx ? refs[loc.afterRefIdx] : null) as HTMLElement;
|
||||
// const afterNode = document.createTextNode("");
|
||||
// target.parentElement!.insertBefore(afterNode, target.nextSibling);
|
||||
// refs[loc.afterRefIdx!] = afterNode;
|
||||
child.isOnlyChild = loc.isOnlyChild;
|
||||
(child as any).hydrate(target.parentElement, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
|
||||
@@ -42,3 +42,7 @@ export function withKey(vnode: VNode, key: any) {
|
||||
vnode.key = key;
|
||||
return vnode;
|
||||
}
|
||||
|
||||
export function hydrate(vnode: VNode, target: HTMLElement) {
|
||||
(vnode as any).hydrate(target.parentElement, target);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ abstract class VSimpleNode {
|
||||
this.el = node;
|
||||
}
|
||||
|
||||
hydrate(parent: HTMLElement, elem: Node) {
|
||||
this.parentEl = parent;
|
||||
this.el = elem;
|
||||
// this.mountNode(elem, parent, elem.nextSibling);
|
||||
}
|
||||
|
||||
moveBefore(other: VText | null, afterNode: Node | null) {
|
||||
const target = other ? other.el! : afterNode;
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
|
||||
@@ -272,6 +272,15 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
|
||||
this.fiber = null;
|
||||
}
|
||||
|
||||
hydrate(parent: HTMLElement, el: HTMLElement) {
|
||||
const bdom = this.fiber!.bdom!;
|
||||
this.bdom = bdom;
|
||||
(bdom as any).hydrate(parent, el);
|
||||
this.status = STATUS.MOUNTED;
|
||||
this.fiber!.appliedToDom = true;
|
||||
this.fiber = null;
|
||||
}
|
||||
|
||||
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
|
||||
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BDom, mount } from "../blockdom";
|
||||
import { BDom, hydrate, mount } from "../blockdom";
|
||||
import type { ComponentNode } from "./component_node";
|
||||
import { fibersInError, handleError } from "./error_handling";
|
||||
import { STATUS } from "./status";
|
||||
@@ -144,15 +144,18 @@ type Position = "first-child" | "last-child";
|
||||
|
||||
export interface MountOptions {
|
||||
position?: Position;
|
||||
hydrate?: boolean;
|
||||
}
|
||||
|
||||
export class MountFiber extends RootFiber {
|
||||
target: HTMLElement;
|
||||
position: Position;
|
||||
hydrate?: boolean;
|
||||
|
||||
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
|
||||
super(node, null);
|
||||
this.target = target;
|
||||
this.hydrate = options.hydrate;
|
||||
this.position = options.position || "last-child";
|
||||
}
|
||||
complete() {
|
||||
@@ -168,7 +171,9 @@ export class MountFiber extends RootFiber {
|
||||
node.updateDom();
|
||||
} else {
|
||||
node.bdom = this.bdom;
|
||||
if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||
if (this.hydrate) {
|
||||
hydrate(node.bdom!, this.target);
|
||||
} else if (this.position === "last-child" || this.target.childNodes.length === 0) {
|
||||
mount(node.bdom!, this.target);
|
||||
} else {
|
||||
const firstChild = this.target.childNodes[0];
|
||||
|
||||
@@ -1,62 +1,97 @@
|
||||
import { getCurrent } from "./component_node";
|
||||
import { nodeErrorHandlers } from "./error_handling";
|
||||
|
||||
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||
const error = new Error(`The following error occurred in ${hookName}: `) as Error & {
|
||||
cause: any;
|
||||
};
|
||||
return (...args: any[]) => {
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
return result.catch((cause) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
} catch (cause) {
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function onWillStart(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
node.willStart.push(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
|
||||
}
|
||||
|
||||
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
node.willUpdateProps.push(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
|
||||
}
|
||||
|
||||
export function onMounted(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
node.mounted.push(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
|
||||
}
|
||||
|
||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
||||
const node = getCurrent();
|
||||
node.willPatch.unshift(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
|
||||
}
|
||||
|
||||
export function onPatched(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
node.patched.push(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
|
||||
}
|
||||
|
||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
node.willUnmount.unshift(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
|
||||
}
|
||||
|
||||
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
||||
const node = getCurrent();
|
||||
node.willDestroy.push(fn.bind(node.component));
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
|
||||
}
|
||||
|
||||
export function onWillRender(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const renderFn = node.renderFn;
|
||||
node.renderFn = () => {
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.renderFn = decorate(() => {
|
||||
fn.call(node.component);
|
||||
return renderFn();
|
||||
};
|
||||
}, "onWillRender");
|
||||
}
|
||||
|
||||
export function onRendered(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const renderFn = node.renderFn;
|
||||
node.renderFn = () => {
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.renderFn = decorate(() => {
|
||||
const result = renderFn();
|
||||
fn.call(node.component);
|
||||
return result;
|
||||
};
|
||||
}, "onRendered");
|
||||
}
|
||||
|
||||
type OnErrorCallback = (error: any) => void | any;
|
||||
|
||||
@@ -169,6 +169,15 @@ describe("properties", () => {
|
||||
expect(input.value).toBe("potato");
|
||||
});
|
||||
|
||||
test("input with value attribute, and undefined given", () => {
|
||||
const block = createBlock(`<input block-attribute-0="value"/>`);
|
||||
|
||||
const tree = block([undefined]);
|
||||
mount(tree, fixture);
|
||||
const input = fixture.querySelector("input")!;
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
|
||||
test("input type=checkbox with checked attribute", () => {
|
||||
// render input with initial value
|
||||
const block = createBlock(`<input type="checkbox" block-attribute-0="checked"/>`);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { hydrate, patch, text, createBlock } from "../../src/blockdom";
|
||||
import { makeTestFixture } from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("hydration", () => {
|
||||
test("simple text node", async () => {
|
||||
fixture.innerHTML = "some text";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const tree = text("some text");
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("some text");
|
||||
expect(tree.el).toBe(target);
|
||||
|
||||
patch(tree, text("checkmate"));
|
||||
expect(fixture.innerHTML).toBe("checkmate");
|
||||
});
|
||||
|
||||
test("simple static block", async () => {
|
||||
fixture.innerHTML = "<div>some text</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const block = createBlock("<div>some text</div>");
|
||||
|
||||
const tree = block();
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("<div>some text</div>");
|
||||
expect(tree.el).toBe(target);
|
||||
});
|
||||
|
||||
test("simple dynamic block", async () => {
|
||||
fixture.innerHTML = "<div>some text</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const block = createBlock("<div><block-text-0/></div>");
|
||||
|
||||
const tree = block(["some text"]);
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("<div>some text</div>");
|
||||
expect(tree.el).toBe(target);
|
||||
|
||||
patch(tree, block(["giuoco piano"]));
|
||||
expect(fixture.innerHTML).toBe("<div>giuoco piano</div>");
|
||||
});
|
||||
|
||||
test("block with sub block", async () => {
|
||||
fixture.innerHTML = "<div>queen<p>gambit</p></div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
const block1 = createBlock("<div><block-text-0/><block-child-0/></div>");
|
||||
const block2 = createBlock("<p><block-text-0/></p>");
|
||||
|
||||
const tree = block1(["queen"], [block2(["gambit"])]);
|
||||
expect(tree.el).toBe(undefined);
|
||||
hydrate(tree, target);
|
||||
expect(fixture.innerHTML).toBe("<div>queen<p>gambit</p></div>");
|
||||
expect(tree.el).toBe(target);
|
||||
|
||||
patch(tree, block1(["king"], [block2(["pawn"])]));
|
||||
expect(fixture.innerHTML).toBe("<div>king<p>pawn</p></div>");
|
||||
});
|
||||
});
|
||||
@@ -1092,6 +1092,19 @@ exports[`errors and promises an error in mounted call will reject the mount prom
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`errors and promises an error in onMounted callback will have the component's setup in its stack trace 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>abc</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`errors and promises an error in patched call will reject the render promise 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`hydration can hydrate a component with a handler 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div block-handler-0=\\"click\\"><block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['inc'], ctx];
|
||||
let txt1 = ctx['state'].value;
|
||||
return block1([hdlr1, txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hydration can hydrate a component with a sub component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2 = component(\`Counter\`, {}, key + \`__1\`, node, ctx);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hydration can hydrate a component with a sub component 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let hdlr1 = [ctx['inc'], ctx];
|
||||
let txt1 = ctx['state'].value;
|
||||
return block1([hdlr1, txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`hydration can hydrate a simple static component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>giuoco pianissimo</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -410,6 +410,24 @@ exports[`t-model directive on an textarea 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model on an input with an undefined value 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
let { toNumber } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<input block-attribute-0=\\"value\\" block-handler-1=\\"input\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const bExpr1 = ctx['state'];
|
||||
const expr1 = 'text';
|
||||
let attr1 = bExpr1[expr1];
|
||||
let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }];
|
||||
return block1([attr1, hdlr1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-model directive t-model on select with static options 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -82,8 +82,6 @@ describe("basics", () => {
|
||||
});
|
||||
|
||||
test("display a nice error if it cannot find component (in dev mode)", async () => {
|
||||
const info = console.info;
|
||||
console.info = jest.fn(() => {}); // dev mode message
|
||||
class SomeComponent extends Component {}
|
||||
class Parent extends Component {
|
||||
static template = xml`<SomeMispelledComponent />`;
|
||||
@@ -91,7 +89,7 @@ describe("basics", () => {
|
||||
}
|
||||
let error: Error;
|
||||
try {
|
||||
await mount(Parent, fixture, { dev: true });
|
||||
await mount(Parent, fixture, { test: true });
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
@@ -100,8 +98,6 @@ describe("basics", () => {
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
expect(console.info).toBeCalledTimes(1);
|
||||
console.info = info;
|
||||
});
|
||||
|
||||
test("simple catchError", async () => {
|
||||
@@ -179,6 +175,30 @@ describe("errors and promises", () => {
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("an error in onMounted callback will have the component's setup in its stack trace", async () => {
|
||||
class App extends Component {
|
||||
static template = xml`<div>abc</div>`;
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let error: Error;
|
||||
try {
|
||||
await mount(App, fixture, { test: true });
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.stack).toContain("App.setup");
|
||||
expect(error!.stack).toContain("error_handling.test.ts");
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("an error in willPatch call will reject the render promise", async () => {
|
||||
class Root extends Component {
|
||||
static template = xml`<div><t t-esc="val"/></div>`;
|
||||
@@ -191,13 +211,13 @@ describe("errors and promises", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = await mount(Root, fixture);
|
||||
const root = await mount(Root, fixture, { test: true });
|
||||
root.val = 4;
|
||||
let error: Error;
|
||||
root.render();
|
||||
await nextTick();
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe("boom");
|
||||
expect(error!.message).toBe(`The following error occurred in onWillPatch: "boom"`);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -214,13 +234,13 @@ describe("errors and promises", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = await mount(Root, fixture);
|
||||
const root = await mount(Root, fixture, { test: true });
|
||||
root.val = 4;
|
||||
let error: Error;
|
||||
root.render();
|
||||
await nextTick();
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe("boom");
|
||||
expect(error!.message).toBe(`The following error occurred in onPatched: "boom"`);
|
||||
expect(mockConsoleError).toBeCalledTimes(0);
|
||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -296,7 +316,6 @@ describe("errors and promises", () => {
|
||||
});
|
||||
|
||||
test("errors in mounted and in willUnmount", async () => {
|
||||
expect.assertions(4);
|
||||
class Example extends Component {
|
||||
static template = xml`<div/>`;
|
||||
val: any;
|
||||
@@ -313,9 +332,11 @@ describe("errors and promises", () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await mount(Example, fixture);
|
||||
await mount(Example, fixture, { test: true });
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe("Error in mounted");
|
||||
expect((e as Error).message).toBe(
|
||||
`The following error occurred in onMounted: "Error in mounted"`
|
||||
);
|
||||
}
|
||||
// 1 additional error is logged because the destruction of the app causes
|
||||
// the onWillUnmount hook to be called and to fail
|
||||
@@ -385,14 +406,14 @@ describe("can catch errors", () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
let e: any = null;
|
||||
let e: Error;
|
||||
try {
|
||||
await mount(Root, fixture);
|
||||
await mount(Root, fixture, { test: true });
|
||||
} catch (error) {
|
||||
e = error;
|
||||
e = error as Error;
|
||||
}
|
||||
expect(e.message).toBe(
|
||||
"No active component (a hook function should only be called in 'setup')"
|
||||
expect(e!.message).toBe(
|
||||
`The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, mount, useState, xml } from "../../src";
|
||||
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
describe("hydration", () => {
|
||||
test("can hydrate a simple static component", async () => {
|
||||
fixture.innerHTML = "<div>giuoco pianissimo</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
class Test extends Component {
|
||||
static template = xml`<div>giuoco pianissimo</div>`;
|
||||
}
|
||||
|
||||
await mount(Test, target, { hydrate: true });
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>giuoco pianissimo</div>");
|
||||
expect(fixture.firstChild).toBe(target);
|
||||
});
|
||||
|
||||
test("can hydrate a component with a handler", async () => {
|
||||
fixture.innerHTML = "<div>0</div>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`<div t-on-click="inc"><t t-esc="state.value"/></div>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
inc() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Counter, target, { hydrate: true });
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div>0</div>");
|
||||
expect(fixture.firstChild).toBe(target);
|
||||
|
||||
target.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
});
|
||||
|
||||
test("can hydrate a component with a sub component", async () => {
|
||||
fixture.innerHTML = "<p><button>0</button></p>";
|
||||
const target = fixture.firstChild as any;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`<button t-on-click="inc"><t t-esc="state.value"/></button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
inc() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<p><Counter/></p>`;
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
await mount(Parent, target, { hydrate: true });
|
||||
|
||||
expect(fixture.innerHTML).toBe("<p><button>0</button></p>");
|
||||
expect(fixture.firstChild).toBe(target);
|
||||
|
||||
target.querySelector("button")!.click();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<p><button>1</button></p>");
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ let mockConsoleWarn: any;
|
||||
|
||||
beforeAll(() => {
|
||||
console.info = (message: any) => {
|
||||
if (message === DEV_MSG) {
|
||||
if (message === DEV_MSG()) {
|
||||
return;
|
||||
}
|
||||
info(message);
|
||||
|
||||
@@ -30,6 +30,19 @@ describe("t-model directive", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
||||
});
|
||||
|
||||
test("t-model on an input with an undefined value", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<input t-model="state.text"/>`;
|
||||
state = useState({ text: undefined });
|
||||
}
|
||||
await mount(SomeComponent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe("<input>");
|
||||
|
||||
const input = fixture.querySelector("input")!;
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
|
||||
test("basic use, on an input with bracket expression", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
|
||||
@@ -29,7 +29,7 @@ snapshotEverything();
|
||||
|
||||
beforeAll(() => {
|
||||
console.info = (message: any) => {
|
||||
if (message === DEV_MSG) {
|
||||
if (message === DEV_MSG()) {
|
||||
return;
|
||||
}
|
||||
info(message);
|
||||
|
||||
@@ -24,8 +24,8 @@ class Root extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: true });`;
|
||||
// dev=false for benchmarking. we don't want to benchmark dev code!
|
||||
mount(Root, document.body, { templates: TEMPLATES, dev: false });`;
|
||||
|
||||
const COMPONENTS_XML = /*xml*/`
|
||||
<templates>
|
||||
|
||||
Reference in New Issue
Block a user