Compare commits

...

6 Commits

Author SHA1 Message Date
Géry Debongnie da5fa6d45e poc: xml function returns function 2022-03-16 11:46:30 +01:00
Samuel Degueldre 77ff5ee895 [IMP] component: emit warning when async hooks take too long
This commit adds a warning when an async hook
(onWillUpdateProps/onWillStart) takes longer than 3 seconds, as these
hooks block the rendering and patching of the application, it is rarely
desirable and often a sign of a deadlock. This warning will contain the
stack trace of the call to the hook to help in debugging.
2022-03-14 09:51:47 +01:00
Géry Debongnie 47c6d6cc3c [IMP] component: add name property on nodes for debug purposes
also, improves the implementation of subscriptions
2022-03-11 15:14:55 +01:00
Samuel Degueldre 0625b5883a [IMP] reactivity: add missing support for forEach method 2022-03-11 14:02:42 +01:00
Géry Debongnie 53ab54b1ec [FIX] useEffect: properly handle errors in effect function
Before this commit, if the effect function would throw, then the cleanup
function would not be properly assigned, which caused additional errors
later, when the cleanup code would try to call it.

closes #1149
2022-03-08 15:23:49 +01:00
Samuel Degueldre 4770b91faa [IMP] doc: document dev mode 2022-03-08 14:45:44 +01:00
18 changed files with 438 additions and 126 deletions
+1
View File
@@ -82,6 +82,7 @@ Are you new to Owl? This is the place to start!
- [Component](doc/reference/component.md)
- [Component Lifecycle](doc/reference/component.md#lifecycle)
- [Concurrency Model](doc/reference/concurrency_model.md)
- [Dev mode](doc/reference/app.md#dev-mode)
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
- [Environment](doc/reference/environment.md)
- [Error Handling](doc/reference/error_handling.md)
+13 -4
View File
@@ -51,11 +51,10 @@ The `config` object is an object with some of the following keys:
- **`env (object)`**: if given, this will be the shared `env` given to each component
- **`props (object)`**: the props given to the root component
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in `dev`
mode, which activates some additional checks (in particular, the props validation
code is only performed in dev mode)
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
[`dev` mode](#dev-mode);
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
that Owll will not log a message to warn that Owl is in `dev` mode.
that Owl will not log a message to warn that Owl is in `dev` mode.
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
@@ -110,3 +109,13 @@ const { loadFile, mount } = owl;
mount(Root, document.body, { env });
})();
```
## Dev mode
Dev mode activates some additional checks and developer amenities:
- [Props validation](./props.md#props-validation) is performed
- [t-foreach](./templates.md#loops) loops check for key unicity
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
- onWillStart and onWillUpdateProps will emit a warning in the console when they
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
+9 -6
View File
@@ -1,10 +1,9 @@
import { createBlock, html, list, multi, text, toggler, comment } from "../blockdom";
import { compile, Template } from "../compiler";
import { compile, Template, TemplateFunction } from "../compiler";
import { markRaw } from "../reactivity";
import { Portal } from "../portal";
import { component, getCurrent } from "../component/component_node";
import { helpers } from "./template_helpers";
import { globalTemplates } from "../utils";
const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
@@ -48,7 +47,7 @@ function makeHelpers(getTemplate: (name: string) => Template): any {
markRaw,
getTemplate,
call: (owner: any, subTemplate: string, ctx: any, parent: any, key: any) => {
const template = getTemplate(subTemplate);
const template = typeof subTemplate === "string" ? getTemplate(subTemplate) : subTemplate;
return toggler(subTemplate, template.call(owner, ctx, parent, key));
},
});
@@ -63,11 +62,12 @@ export interface TemplateSetConfig {
export class TemplateSet {
dev: boolean;
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
rawTemplates: any = {}; //typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
helpers: any;
bdom: any = bdom;
constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
@@ -102,8 +102,11 @@ export class TemplateSet {
}
}
getTemplate(name: string): Template {
getTemplate(name: string | any): Template {
if (!(name in this.templates)) {
if (typeof name === "function") {
return name(this);
}
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
let extraInfo = "";
@@ -126,7 +129,7 @@ export class TemplateSet {
return this.templates[name];
}
_compileTemplate(name: string, template: string | Element) {
_compileTemplate(name: string, template: string | Element): TemplateFunction {
return compile(template, {
name,
dev: this.dev,
+3 -1
View File
@@ -1,4 +1,6 @@
import type { ComponentNode } from "./component_node";
import type { TemplateFunction } from "../compiler/index";
import type { App } from "..";
// -----------------------------------------------------------------------------
// Component Class
@@ -20,7 +22,7 @@ export type ComponentConstructor<P extends Props = any, E = any> = (new (
StaticComponentProperties;
export class Component<Props = any, Env = any> {
static template: string = "";
static template: string | ((app: App) => TemplateFunction) = "";
static props?: any;
static defaultProps?: any;
+12 -7
View File
@@ -58,13 +58,6 @@ export function useState<T extends object>(state: T): Reactive<T> | NonReactive<
batchedRenderFunctions.set(node, render);
// manual implementation of onWillDestroy to break cyclic dependency
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
if (node.app.dev) {
Object.defineProperty(node, "subscriptions", {
get() {
return getSubscriptions(render);
},
});
}
}
return reactive(state, render);
}
@@ -394,4 +387,16 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
}
}
}
// ---------------------------------------------------------------------------
// Some debug helpers
// ---------------------------------------------------------------------------
get name(): string {
return this.component.constructor.name;
}
get subscriptions(): ReturnType<typeof getSubscriptions> {
const render = batchedRenderFunctions.get(this);
return render ? getSubscriptions(render) : [];
}
}
+14
View File
@@ -1,14 +1,28 @@
import { getCurrent } from "./component_node";
import { nodeErrorHandlers } from "./error_handling";
const TIMEOUT = Symbol("timeout");
function wrapError(fn: (...args: any[]) => any, hookName: string) {
const error = new Error(`The following error occurred in ${hookName}: `) as Error & {
cause: any;
};
const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
const node = getCurrent();
return (...args: any[]) => {
try {
const result = fn(...args);
if (result instanceof Promise) {
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
const fiber = node.fiber;
Promise.race([
result,
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber) {
console.warn(timeoutError);
}
});
}
return result.catch((cause) => {
error.cause = cause;
if (cause instanceof Error) {
+7 -6
View File
@@ -57,7 +57,6 @@ export function useChildSubEnv(envExtension: Env) {
// useEffect
// -----------------------------------------------------------------------------
const NO_OP = () => {};
/**
* @param {...any} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
@@ -78,11 +77,11 @@ type Effect = (...dependencies: any[]) => void | (() => void);
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
let cleanup: () => void;
let cleanup: (() => void) | void;
let dependencies: any[];
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies) || NO_OP;
cleanup = effect(...dependencies);
});
onPatched(() => {
@@ -90,12 +89,14 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapply) {
dependencies = newDeps;
cleanup();
cleanup = effect(...dependencies) || NO_OP;
if (cleanup) {
cleanup();
}
cleanup = effect(...dependencies);
}
});
onWillUnmount(() => cleanup());
onWillUnmount(() => cleanup && cleanup());
}
// -----------------------------------------------------------------------------
+24
View File
@@ -308,6 +308,28 @@ function makeIteratorObserver(
}
};
}
/**
* Creates a forEach function that will delegate to forEach on the underlying
* collection while observing key changes, and keys as they're iterated over,
* and making the passed keys/values reactive.
*
* @param target @see reactive
* @param callback @see reactive
*/
function makeForEachObserver(target: any, callback: Callback) {
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
observeTargetKey(target, KEYCHANGES, callback);
target.forEach(function (val: any, key: any, targetObj: any) {
observeTargetKey(target, key, callback);
forEachCb.call(
thisArg,
possiblyReactive(val, callback),
possiblyReactive(key, callback),
possiblyReactive(targetObj, callback)
);
}, thisArg);
};
}
/**
* Creates a function that will delegate to an underlying method, and check if
* that method has modified the presence or value of a key, and notify the
@@ -370,6 +392,7 @@ const rawTypeToFuncHandlers = {
values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target),
get size() {
observeTargetKey(target, KEYCHANGES, callback);
@@ -385,6 +408,7 @@ const rawTypeToFuncHandlers = {
values: makeIteratorObserver("values", target, callback),
entries: makeIteratorObserver("entries", target, callback),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
forEach: makeForEachObserver(target, callback),
clear: makeClearNotifier(target),
get size() {
observeTargetKey(target, KEYCHANGES, callback);
+33 -5
View File
@@ -75,13 +75,41 @@ export function markup(value: any) {
// -----------------------------------------------------------------------------
// xml tag helper
// -----------------------------------------------------------------------------
export const globalTemplates: { [key: string]: string | Element } = {};
export function xml(...args: Parameters<typeof String.raw>) {
const name = `__template__${xml.nextId++}`;
export function xml(...args: Parameters<typeof String.raw>): any {
const subTemplates: any = {};
let hasSubTemplates = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg === "function") {
hasSubTemplates = true;
const id = `__template__${xml.nextId++}`;
subTemplates[id] = arg;
args[i] = `{{__owl__.subTemplates.${id}(__owl__.app)}}`;
}
}
const value = String.raw(...args);
globalTemplates[name] = value;
return name;
let cache = new WeakMap();
return (app: any) => {
debugger;
let renderFn = cache.get(app);
if (!renderFn) {
let templateFn = app._compileTemplate("", value);
renderFn = templateFn(app.bdom, app.helpers);
if (hasSubTemplates) {
let origRenderFn = renderFn;
renderFn = (context: any, vnode: any, key?: any) => {
debugger;
context.__owl__.subTemplates = subTemplates;
return origRenderFn(context, vnode, key);
};
}
cache.set(app, renderFn);
}
return renderFn;
};
}
xml.nextId = 1;
@@ -169,11 +169,11 @@ exports[`basics can inject values in tagged templates 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { call } = helpers;
return function template(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__1(ctx['__owl__'].app));
return call(this, template1, ctx, node, key + \`__1\`);
}
}"
`;
@@ -301,6 +301,19 @@ exports[`hooks useEffect hook effect with empty dependency list never reruns 1`]
}"
`;
exports[`hooks useEffect hook properly behaves when the effect function throws 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`hooks useExternalListener 1`] = `
"function anonymous(bdom, helpers
) {
@@ -658,6 +658,43 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
}"
`;
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<span/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const props1 = {prop: ctx['state'].prop};
helpers.validateProps(\`Child\`, props1, ctx);
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
}
}"
`;
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`\`);
}
}"
`;
exports[`lifecycle hooks willPatch, patched hook are called on subsubcomponents, in proper order 1`] = `
"function anonymous(bdom, helpers
) {
@@ -249,11 +249,11 @@ exports[`slots can use t-call in default-content of t-slot 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { callSlot, call } = helpers;
function defaultContent1(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__3(ctx['__owl__'].app));
return call(this, template1, ctx, node, key + \`__1\`);
}
return function template(ctx, node, key = \\"\\") {
@@ -772,12 +772,12 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, getTemplate, markRaw } = helpers;
const callTemplate_1 = getTemplate(\`__template__1002\`);
let { capture, call, markRaw } = helpers;
function slot1(ctx, node, key = \\"\\") {
const b2 = text(\`[A]\`);
const b3 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__5(ctx['__owl__'].app));
const b3 = call(this, template1, ctx, node, key + \`__1\`);
return multi([b2, b3]);
}
@@ -790,41 +790,6 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__1001\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
const b2 = text(\`[sub1] \`);
setContextValue(ctx, \\"dummy\\", ctx['validate']);
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"v\\", ctx['props'].number);
const b3 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
return multi([b2, b3]);
}
}"
`;
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 5`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`[sub2\`);
const b3 = text(ctx['v']);
const b4 = text(\`]\`);
return multi([b2, b3, b4]);
}
}"
`;
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 6`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -836,7 +801,7 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
}"
`;
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 7`] = `
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 5`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -850,6 +815,41 @@ exports[`slots mix of slots, t-call, t-call with body, and giving own props chil
}"
`;
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 6`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { isBoundary, withDefault, setContextValue, call } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
const b2 = text(\`[sub1] \`);
setContextValue(ctx, \\"dummy\\", ctx['validate']);
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"v\\", ctx['props'].number);
const template1 = (ctx['__owl__'].subTemplates.__template__4(ctx['__owl__'].app));
const b3 = call(this, template1, ctx, node, key + \`__1\`);
return multi([b2, b3]);
}
}"
`;
exports[`slots mix of slots, t-call, t-call with body, and giving own props child 7`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`[sub2\`);
const b3 = text(ctx['v']);
const b4 = text(\`]\`);
return multi([b2, b3, b4]);
}
}"
`;
exports[`slots multiple roots are allowed in a default slot 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1486,13 +1486,13 @@ exports[`slots slot and (inline) t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, getTemplate, markRaw } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { capture, call, markRaw } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function slot1(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__2(ctx['__owl__'].app));
return call(this, template1, ctx, node, key + \`__1\`);
}
return function template(ctx, node, key = \\"\\") {
@@ -1505,19 +1505,6 @@ exports[`slots slot and (inline) t-call 1`] = `
exports[`slots slot and (inline) t-call 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<p>sokka</p>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`slots slot and (inline) t-call 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
@@ -1531,17 +1518,30 @@ exports[`slots slot and (inline) t-call 3`] = `
}"
`;
exports[`slots slot and (inline) t-call 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<p>sokka</p>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`slots slot and t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, getTemplate, markRaw } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { capture, call, markRaw } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function slot1(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__1(ctx['__owl__'].app));
return call(this, template1, ctx, node, key + \`__1\`);
}
return function template(ctx, node, key = \\"\\") {
@@ -1556,11 +1556,13 @@ exports[`slots slot and t-call 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<p>sokka</p>\`);
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
const b2 = callSlot(ctx, node, key, 'default', false, null);
return block1([], [b2]);
}
}"
`;
@@ -1569,13 +1571,11 @@ exports[`slots slot and t-call 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`);
let block1 = createBlock(\`<p>sokka</p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callSlot(ctx, node, key, 'default', false, null);
return block1([], [b2]);
return block1();
}
}"
`;
@@ -116,13 +116,13 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { call } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__1(ctx['__owl__'].app));
const b2 = call(this, template1, ctx, node, key + \`__1\`);
let txt1 = ctx['counter'];
return block1([txt1], [b2]);
}
@@ -147,13 +147,13 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { call } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__6(ctx['__owl__'].app));
const b2 = call(this, template1, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
@@ -178,13 +178,13 @@ exports[`t-call parent is set within t-call 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { call } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__3(ctx['__owl__'].app));
const b2 = call(this, template1, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
@@ -218,11 +218,11 @@ exports[`t-call parent is set within t-call with no parentNode 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { getTemplate } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { call } = helpers;
return function template(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
const template1 = (ctx['__owl__'].subTemplates.__template__5(ctx['__owl__'].app));
return call(this, template1, ctx, node, key + \`__1\`);
}
}"
`;
@@ -353,8 +353,7 @@ exports[`t-call t-call in t-foreach and children component 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { prepareList, getTemplate, withKey } = helpers;
const callTemplate_1 = getTemplate(\`__template__999\`);
let { prepareList, call, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
@@ -368,7 +367,8 @@ exports[`t-call t-call in t-foreach and children component 1`] = `
ctx[\`val_index\`] = i1;
ctx[\`val_value\`] = k_block2[i1];
const key1 = ctx['val'];
c_block2[i1] = withKey(callTemplate_1.call(this, ctx, node, key + \`__1__\${key1}\`), key1);
const template1 = (ctx['__owl__'].subTemplates.__template__4(ctx['__owl__'].app));
c_block2[i1] = withKey(call(this, template1, ctx, node, key + \`__1__\${key1}\`), key1);
}
const b2 = list(c_block2);
return block1([], [b2]);
+33 -3
View File
@@ -444,6 +444,7 @@ describe("hooks", () => {
let cleanupRun = 0;
let steps = [];
class MyComponent extends Component {
static template = xml`<div/>`;
state = useState({
value: 0,
});
@@ -455,7 +456,6 @@ describe("hooks", () => {
});
}
}
MyComponent.template = xml`<div/>`;
const component = await mount(MyComponent, fixture);
@@ -512,6 +512,7 @@ describe("hooks", () => {
test("dependencies prevent effects from rerunning when unchanged", async () => {
let steps = [];
class MyComponent extends Component {
static template = xml`<div/>`;
state = useState({
a: 0,
b: 0,
@@ -540,7 +541,6 @@ describe("hooks", () => {
);
}
}
MyComponent.template = xml`<div/>`;
steps.push("before mount");
const component = await mount(MyComponent, fixture);
steps.push("after mount");
@@ -597,6 +597,7 @@ describe("hooks", () => {
test("effect with empty dependency list never reruns", async () => {
let steps = [];
class MyComponent extends Component {
static template = xml`<div t-esc="state.value"/>`;
state = useState({
value: 0,
});
@@ -610,7 +611,6 @@ describe("hooks", () => {
);
}
}
MyComponent.template = xml`<div t-esc="state.value"/>`;
const component = await mount(MyComponent, fixture);
@@ -632,5 +632,35 @@ describe("hooks", () => {
"cleaning up for 1",
]);
});
test("properly behaves when the effect function throws", async () => {
let originalconsoleError = console.error;
let originalconsoleWarn = console.warn;
console.error = jest.fn(() => {});
console.warn = jest.fn(() => {});
class MyComponent extends Component {
static template = xml`<div/>`;
setup() {
useEffect(
() => {
throw new Error("Intentional error");
},
() => []
);
}
}
try {
await mount(MyComponent, fixture);
} catch (e: any) {
expect(e.message).toBe("Intentional error");
}
// no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0);
console.error = originalconsoleError;
// 1 console.warn because app is destroyed
expect(console.warn).toHaveBeenCalledTimes(1);
console.warn = originalconsoleWarn;
});
});
});
+78
View File
@@ -14,6 +14,7 @@ import {
logStep,
makeDeferred,
makeTestFixture,
nextMicroTick,
nextTick,
snapshotEverything,
useLogLifecycle,
@@ -104,6 +105,83 @@ describe("lifecycle hooks", () => {
await mount(Test, fixture);
});
test("timeout in onWillStart emits a warning", async () => {
const { warn } = console;
let warnArgs: any[];
console.warn = jest.fn((...args) => (warnArgs = args));
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
window.setTimeout = ((cb: any) => {
timeoutCbs[++timeoutId] = cb;
return timeoutId;
}) as any;
class Test extends Component {
static template = xml`<span/>`;
setup() {
onWillStart(() => new Promise(() => {}));
}
}
mount(Test, fixture, { test: true });
nextTick();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await nextMicroTick();
await nextMicroTick();
expect(console.warn).toHaveBeenCalledTimes(1);
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
console.warn = warn;
window.setTimeout = setTimeout;
});
test("timeout in onWillUpdateProps emits a warning", async () => {
class Child extends Component {
static template = xml``;
setup() {
onWillUpdateProps(() => new Promise(() => {}));
}
}
class Parent extends Component {
static template = xml`<Child prop="state.prop"/>`;
static components = { Child };
state = useState({ prop: 1 });
}
const parent = await mount(Parent, fixture, { test: true });
const { warn } = console;
let warnArgs: any[];
console.warn = jest.fn((...args) => (warnArgs = args));
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
window.setTimeout = ((cb: any) => {
timeoutCbs[++timeoutId] = cb;
return timeoutId;
}) as any;
parent.state.prop = 2;
let tick = nextTick();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await tick;
tick = nextTick();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await tick;
expect(console.warn).toHaveBeenCalledTimes(1);
expect(warnArgs![0]!.message).toBe(
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
);
console.warn = warn;
window.setTimeout = setTimeout;
});
test("mounted hook is called if mounted in DOM", async () => {
let mounted = false;
class Test extends Component {
+19 -9
View File
@@ -13,13 +13,13 @@ import {
onWillUpdateProps,
status,
useComponent,
xml,
} from "../src";
import { helpers } from "../src/app/template_helpers";
import { TemplateSet } from "../src/app/template_set";
import { BDom } from "../src/blockdom";
import { compile } from "../src/compiler";
import { globalTemplates } from "../src/utils";
import { Portal } from "../src/portal";
// import { globalTemplates } from "../src/utils";
const mount = blockDom.mount;
@@ -39,9 +39,6 @@ export function makeTestFixture() {
return fixture;
}
beforeEach(() => {
xml.nextId = 999;
});
export async function nextTick(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
@@ -49,7 +46,7 @@ export async function nextTick(): Promise<void> {
interface Deferred extends Promise<any> {
resolve(val?: any): void;
reject(): void;
reject(val?: any): void;
}
export function makeDeferred(): Deferred {
@@ -117,16 +114,29 @@ export function snapshotEverything() {
// this function has already been called
return;
}
const globalTemplateNames = new Set(Object.keys(globalTemplates));
shouldSnapshot = true;
beforeEach(() => {
snapshottedTemplates.clear();
});
const getTemplate = TemplateSet.prototype.getTemplate;
TemplateSet.prototype.getTemplate = function (this: any, name: string) {
if (name === Portal.template) {
this.skipSnapshot = true;
}
const result = getTemplate.call(this, name);
this.skipSnapshot = false;
return result;
};
const originalCompileTemplate = TemplateSet.prototype._compileTemplate;
TemplateSet.prototype._compileTemplate = function (name: string, template: string | Element) {
TemplateSet.prototype._compileTemplate = function (
this: any,
name: string,
template: string | Element
) {
const fn = originalCompileTemplate.call(this, "", template);
if (!globalTemplateNames.has(name)) {
if (!this.skipSnapshot) {
expect(fn.toString()).toMatchSnapshot();
}
return fn;
+57
View File
@@ -1245,6 +1245,33 @@ describe("Collections", () => {
reactiveObj.a = 1; // setting same value again shouldn't notify
expect(observer).toHaveBeenCalledTimes(1);
});
test("iterating with forEach returns reactives", async () => {
const keyObj = { a: 2 };
const thisArg = {};
const observer = jest.fn();
const state = reactive(new Set([keyObj]), observer);
let reactiveKeyObj: any, reactiveValObj: any, thisObj: any, mapObj: any;
state.forEach(function (this: any, val, key, map) {
[reactiveValObj, reactiveKeyObj, mapObj, thisObj] = [val, key, map, this];
}, thisArg);
expect(reactiveKeyObj).not.toBe(keyObj);
expect(reactiveValObj).not.toBe(keyObj);
expect(mapObj).toBe(state); // third argument should be the reactive
expect(thisObj).toBe(thisArg); // thisArg should not be made reactive
expect(toRaw(reactiveKeyObj as any)).toBe(keyObj);
expect(toRaw(reactiveValObj as any)).toBe(keyObj);
expect(reactiveKeyObj).toBe(reactiveValObj); // reactiveKeyObj and reactiveValObj should be the same object
reactiveKeyObj!.a = 0;
reactiveValObj!.a = 0;
expect(observer).toHaveBeenCalledTimes(0);
reactiveKeyObj!.a; // observe key "a" in key sub-reactive;
reactiveKeyObj!.a = 1;
expect(observer).toHaveBeenCalledTimes(1);
reactiveKeyObj!.a = 1; // setting same value again shouldn't notify
reactiveValObj!.a = 1;
expect(observer).toHaveBeenCalledTimes(1);
});
});
describe("WeakSet", () => {
@@ -1467,6 +1494,36 @@ describe("Collections", () => {
reactiveValObj.a = 1;
expect(observer).toHaveBeenCalledTimes(2);
});
test("iterating with forEach returns reactives", async () => {
const keyObj = { a: 2 };
const valObj = { a: 2 };
const thisArg = {};
const observer = jest.fn();
const state = reactive(new Map([[keyObj, valObj]]), observer);
let reactiveKeyObj: any, reactiveValObj: any, thisObj: any, mapObj: any;
state.forEach(function (this: any, val, key, map) {
[reactiveValObj, reactiveKeyObj, mapObj, thisObj] = [val, key, map, this];
}, thisArg);
expect(reactiveKeyObj).not.toBe(keyObj);
expect(reactiveValObj).not.toBe(valObj);
expect(mapObj).toBe(state); // third argument should be the reactive
expect(thisObj).toBe(thisArg); // thisArg should not be made reactive
expect(toRaw(reactiveKeyObj as any)).toBe(keyObj);
expect(toRaw(reactiveValObj as any)).toBe(valObj);
reactiveKeyObj!.a = 0;
reactiveValObj!.a = 0;
expect(observer).toHaveBeenCalledTimes(0);
reactiveKeyObj!.a; // observe key "a" in key sub-reactive;
reactiveKeyObj!.a = 1;
expect(observer).toHaveBeenCalledTimes(1);
reactiveValObj!.a; // observe key "a" in val sub-reactive;
reactiveValObj!.a = 1;
expect(observer).toHaveBeenCalledTimes(2);
reactiveKeyObj!.a = 1; // setting same value again shouldn't notify
reactiveValObj!.a = 1;
expect(observer).toHaveBeenCalledTimes(2);
});
});
describe("WeakMap", () => {