Compare commits

...

24 Commits

Author SHA1 Message Date
Géry Debongnie 41344ef4ec [REL] v2.0.0-beta-6
# 2.0.0-beta-6

- fix: stricter check for the component.render deep argument
2022-04-11 11:01:27 +02:00
Géry Debongnie 7d14db7d31 [FIX] component: strict check of deep argument truth value
With this commit, we make sure that the `render` method was explicitely
called with the deep === true argument, instead of assuming that it is a
boolean.  This prevents errors when some unrelated value is given to the
render method. This could happen in some cases, such as

useBus(someBus, 'someevent', this.render)

In that case, the `useBus` code would simply call the this.render with a
customevent, which would be considered truthy before, and not anymore
2022-04-11 10:58:44 +02:00
Géry Debongnie 1179e84971 [REL] v2.0.0-beta-5
# v2.0.0-beta-5

- fix: compiler, components: allow empty slots with default content
- fix: issue with delayed renders being left pending forever
- fix: dynamic t-slot with scope bug
- fix: protect against errors in onWillDestroy
- fix: protect against user code executing in critical sections
- fix: concurrency issue (more robust handling of children per render)
- fix: prevent rendering destroyed children in some cases
2022-04-07 15:36:22 +02:00
Géry Debongnie 24b1ea7604 [FIX] components: prevent rendering destroyed children in some cases 2022-04-01 15:02:57 +02:00
Géry Debongnie 3e4ebb6378 [REF] component: slightly simplify code logic 2022-04-01 15:02:57 +02:00
Géry Debongnie 859748aed9 [FIX] concurrency issue (more robust handling of children per render)
Before this commit, the list of all children was managed at the level of
the root fiber, but this could cause issue when subfibers would be
reused. With this commit, we use the childrenMap object that exists on
each fiber instead.
2022-04-01 13:40:24 +02:00
Géry Debongnie fd13277e1d [FIX] component: protect against user code executing in critical section
Canceling a fiber may cause user code to be run, which means that some
new renderings could be scheduled, but this could interfere with the
current renderings!
2022-04-01 13:40:24 +02:00
Géry Debongnie 7fb166bd50 [FIX] component: protect against errors in onWillDestroy 2022-04-01 13:40:24 +02:00
Lucas Perais (lpe) decf42c742 [FIX] compiler, component: dynamic t-slot with scope
A little typo was preventing a dynamic t-slot with a scope
(`<t t-slot="{{ state.name }}" myScope="someValue" />`)
to work properly.

This commit corrects this.
2022-03-31 16:15:47 +02:00
Géry Debongnie 989b0d6709 [REF] improve children handling in components/fibers 2022-03-31 08:54:59 +02:00
Géry Debongnie 9b93521da4 [FIX] issue with delayed renders being left pending forever 2022-03-31 08:54:59 +02:00
Lucas Perais (lpe) de240b1ebc [FIX] compiler, components: allow empty slot
Before this commit, a t-set-slot that has no content was not even compiled, and thus not passed
to the component for which it has was defined

After this commit, we allow a t-set-slot to have no content (because it can have slot props)
2022-03-29 16:24:05 +02:00
Géry Debongnie 55dbc01a1b [REL] v2.0.0-beta-4
# v2.0.0-beta.4

- fix: useEffect properly handle errors in effect function
- imp: reactivity: add missing support for forEach method
- imp: component: add name property on nodes for debug purposes
- imp: component: emit warning when async hooks take too long
- fix: blockdom: t-att- correcltly sets the value to zero
- fix: reactivity: do not crash when reading reactive frozen objects
- fix: utils: fix calls to batched callback from within the callback
- imp: component: wait for parent rendering to be complete before rendering child
2022-03-29 15:49:50 +02:00
Géry Debongnie e3b1566943 [IMP] component: wait for parent rendering to be complete before doing child
This is a breaking semantic change.  With this commit, the UI is frozen
whenever owl is waiting for a parent to change

Also, this allows Owl not to render components that will be removed
later.
2022-03-29 15:45:27 +02:00
Géry Debongnie 828be28653 [REF] component: introduce RootFiber.setCounter and update scheduler
The goal is to be able to execute code whenever a root fiber is ready,
and before the next animation frame
2022-03-29 15:45:27 +02:00
Samuel Degueldre d80fad760c [FIX] utils: fix calls to batched callback from within the callback
Previously, calling the batched function from within the callback being
batched would fail as it would be treated as part of the same batch.
This commit fixes that by scheduling the reset of the "called" flag
before calling the callback. This means that all microtasks that were
already in the microtask queue when a batch is about to run are treated
as part of the batch, and all microtasks that will be added by the
callback are not.
2022-03-29 09:13:00 +02:00
Samuel Degueldre 7611ea6033 [FIX] reactivity: do not crash when reading reactive frozen objects
This crash was caused by the fact that Proxies *must* return the value of the
property on the target when that property is non-writeable and
non-configurable. Since the reactivity system always attempts to proxify the
value from the target, this crashes.

This commit fixes that by not proxifying such values. This however means that
from that point on, we have escaped the reactivity system and will not
subscribe to any changes in that object or its children.
2022-03-28 13:15:00 +02:00
NsL01 c7d515a6b3 [FIX] doc: fix minor errors in todo app tutorial 2022-03-25 10:34:00 +01:00
Samuel Degueldre d277039b14 [FIX] blockdom: t-att- correcltly sets the value to zero
In 3536f41f00 we added a fallback when setting a
property to a falsy value so that the property was set to the empty string. The
objective being to not get the string "undefined"/"null"/"false" as property
value. However, using t-att- to set a property to 0 is perfectly reasonable and
in fact quite common.
2022-03-18 14:24:54 +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
37 changed files with 2215 additions and 206 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)
+2 -2
View File
@@ -240,7 +240,7 @@ class Task extends Component {
static template = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.title"/></span>
<span><t t-esc="props.task.text"/></span>
</div>`;
static props = ["task"];
}
@@ -743,7 +743,7 @@ the user experience.
```xml
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/>
t-on-click="() => store.toggleTask(props.task)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
```
+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
+2 -1
View File
@@ -76,7 +76,8 @@ The `Component` class has a very small API.
By default, the render initiated by this method will stop at each child
component if their props are (shallow) equal. To force a render to update
all child components, one can use the optional `deep` argument.
all child components, one can use the optional `deep` argument. Note that the
value of the `deep` argument needs to be a boolean, not a truthy value.
## Static Properties
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta-6",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+2 -1
View File
@@ -67,7 +67,7 @@ export class App<
}
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this);
return new ComponentNode(Component, props, this, null, null);
}
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
@@ -101,6 +101,7 @@ export class App<
destroy() {
if (this.root) {
this.scheduler.flush();
this.root.destroy();
}
}
+2 -1
View File
@@ -139,7 +139,8 @@ 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 || "";
// support 0, fallback to empty string for other falsy values
(this as any)[name] = value === 0 ? 0 : value || "";
};
}
+6 -3
View File
@@ -1115,8 +1115,11 @@ export class CodeGenerator {
let slotStr: string[] = [];
for (let slotName in ast.slots) {
const slotAst = ast.slots[slotName];
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
const params = [`__render: ${name}, __ctx: ${ctxStr}`];
const params = [];
if (slotAst.content) {
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
params.push(`__render: ${name}, __ctx: ${ctxStr}`);
}
const scope = ast.slots[slotName].scope;
if (scope) {
params.push(`__scope: "${scope}"`);
@@ -1231,7 +1234,7 @@ export class CodeGenerator {
if (dynamic) {
let name = this.generateId("slot");
this.define(name, slotName);
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`;
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}, ${dynamic}, ${scope}))`;
} else {
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
}
+17 -19
View File
@@ -118,7 +118,7 @@ export interface ASTTCall {
}
interface SlotDefinition {
content: AST;
content: AST | null;
scope: string | null;
on: EventHandlers | null;
attrs: Attrs | null;
@@ -749,26 +749,24 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slotNode.removeAttribute("t-set-slot");
slotNode.remove();
const slotAst = parseNode(slotNode, ctx);
if (slotAst) {
let on: SlotDefinition["on"] = null;
let attrs: Attrs | null = null;
let scope: string | null = null;
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
scope = value;
continue;
} else if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
} else {
attrs = attrs || {};
attrs[attributeName] = value;
}
let on: SlotDefinition["on"] = null;
let attrs: Attrs | null = null;
let scope: string | null = null;
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
scope = value;
continue;
} else if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
} else {
attrs = attrs || {};
attrs[attributeName] = value;
}
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope };
}
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, scope };
}
// default slot
+1 -1
View File
@@ -37,6 +37,6 @@ export class Component<Props = any, Env = any> {
setup() {}
render(deep: boolean = false) {
this.__owl__.render(deep);
this.__owl__.render(deep === true);
}
}
+44 -60
View File
@@ -2,23 +2,16 @@ import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import {
clearReactivesForCallback,
getSubscriptions,
NonReactive,
Reactive,
reactive,
TARGET,
NonReactive,
getSubscriptions,
} from "../reactivity";
import { batched, Callback } from "../utils";
import { Component, ComponentConstructor } from "./component";
import { fibersInError, handleError } from "./error_handling";
import {
Fiber,
makeChildFiber,
makeRootFiber,
MountFiber,
MountOptions,
RootFiber,
} from "./fibers";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status";
@@ -54,17 +47,10 @@ export function useState<T extends object>(state: T): Reactive<T> | NonReactive<
const node = getCurrent();
let render = batchedRenderFunctions.get(node)!;
if (!render) {
render = batched(node.render.bind(node));
render = batched(node.render.bind(node, false));
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);
}
@@ -93,13 +79,8 @@ export function component<P extends object>(
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
if (node) {
if (node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
if (node && node.status === STATUS.DESTROYED) {
node = undefined;
}
if (isDynamic && node && node.component.constructor !== name) {
node = undefined;
@@ -128,11 +109,11 @@ export function component<P extends object>(
throw new Error(`Cannot find the definition of component "${name}"`);
}
}
node = new ComponentNode(C, props, ctx.app, ctx);
node = new ComponentNode(C, props, ctx.app, ctx, key);
ctx.children[key] = node;
node.initiateRender(new Fiber(node, parentFiber));
}
parentFiber.childrenMap[key] = node;
return node;
}
@@ -150,6 +131,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
forceNextRender: boolean = false;
parentKey: string | null;
renderFn: Function;
parent: ComponentNode | null;
@@ -166,10 +148,17 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
constructor(
C: ComponentConstructor<P, E>,
props: P,
app: App,
parent: ComponentNode | null,
parentKey: string | null
) {
currentNode = this;
this.app = app;
this.parent = parent || null;
this.parent = parent;
this.parentKey = parentKey;
this.level = parent ? parent.level + 1 : 0;
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
@@ -200,13 +189,13 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) {
this._render(fiber);
fiber.render();
}
}
async render(deep: boolean = false) {
async render(deep: boolean) {
let current = this.fiber;
if (current && current.root!.locked) {
if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
@@ -228,6 +217,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
const fiber = makeRootFiber(this);
fiber.deep = deep;
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
@@ -245,16 +235,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
// embedded in a rendering coming from above, so the fiber will be rendered
// in the next microtick anyway, so we should not render it again.
if (this.fiber === fiber && (current || !fiber.parent)) {
this._render(fiber);
}
}
_render(fiber: Fiber | RootFiber) {
try {
fiber.bdom = this.renderFn();
fiber.root!.counter--;
} catch (e) {
handleError({ node: this, error: e });
fiber.render();
}
}
@@ -276,8 +257,14 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
for (let child of Object.values(this.children)) {
child._destroy();
}
for (let cb of this.willDestroy) {
cb.call(component);
if (this.willDestroy.length) {
try {
for (let cb of this.willDestroy) {
cb.call(component);
}
} catch (e) {
handleError({ error: e, node: this });
}
}
this.status = STATUS.DESTROYED;
}
@@ -298,7 +285,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
return;
}
component.props = props;
this._render(fiber);
fiber.render();
const parentRoot = parentFiber.root!;
if (this.willPatch.length) {
parentRoot.willPatch.push(fiber);
@@ -348,6 +335,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom.mount(parent, anchor);
this.status = STATUS.MOUNTED;
this.fiber!.appliedToDom = true;
this.children = this.fiber!.childrenMap;
this.fiber = null;
}
@@ -365,10 +353,8 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
}
_patch() {
const hasChildren = Object.keys(this.children).length > 0;
this.children = this.fiber!.childrenMap;
this.bdom!.patch(this!.fiber!.bdom!, hasChildren);
if (hasChildren) {
this.cleanOutdatedChildren();
}
this.fiber!.appliedToDom = true;
this.fiber = null;
}
@@ -381,17 +367,15 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
this.bdom!.remove();
}
cleanOutdatedChildren() {
const children = this.children;
for (const key in children) {
const node = children[key];
const status = node.status;
if (status !== STATUS.MOUNTED) {
delete children[key];
if (status !== STATUS.DESTROYED) {
node.destroy();
}
}
}
// ---------------------------------------------------------------------------
// 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) : [];
}
}
+5 -8
View File
@@ -5,7 +5,7 @@ import type { Fiber } from "./fibers";
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
function _handleError(node: ComponentNode | null, error: any): boolean {
if (!node) {
return false;
}
@@ -16,22 +16,19 @@ function _handleError(node: ComponentNode | null, error: any, isFirstRound = fal
const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) {
let stopped = false;
let handled = false;
// execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) {
try {
errorHandlers[i](error);
stopped = true;
handled = true;
break;
} catch (e) {
error = e;
}
}
if (stopped) {
if (isFirstRound && fiber && fiber.node.fiber) {
fiber.root!.counter--;
}
if (handled) {
return true;
}
}
@@ -54,7 +51,7 @@ export function handleError(params: ErrorParams) {
fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error, true);
const handled = _handleError(node, error);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
+60 -4
View File
@@ -16,8 +16,14 @@ export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
let root = current.root!;
root.counter = root.counter + 1 - cancelFibers(current.children);
// lock root fiber because canceling children fibers may destroy components,
// which means any arbitrary code can be run in onWillDestroy, which may
// trigger new renderings
root.locked = true;
root.setCounter(root.counter + 1 - cancelFibers(current.children));
root.locked = false;
current.children = [];
current.childrenMap = {};
current.bdom = null;
if (fibersInError.has(current)) {
fibersInError.delete(current);
@@ -42,7 +48,11 @@ export function makeRootFiber(node: ComponentNode): Fiber {
function cancelFibers(fibers: Fiber[]): number {
let result = 0;
for (let fiber of fibers) {
fiber.node.fiber = null;
let node = fiber.node;
if (node.status === STATUS.NEW) {
node.destroy();
}
node.fiber = null;
if (fiber.bdom) {
// if fiber has been rendered, this means that the component props have
// been updated. however, this fiber will not be patched to the dom, so
@@ -50,7 +60,7 @@ function cancelFibers(fibers: Fiber[]): number {
// the same props, and skip the render completely. With the next line,
// we kindly request the component code to force a render, so it works as
// expected.
fiber.node.forceNextRender = true;
node.forceNextRender = true;
} else {
result++;
}
@@ -67,6 +77,7 @@ export class Fiber {
children: Fiber[] = [];
appliedToDom = false;
deep: boolean = false;
childrenMap: ComponentNode["children"] = {};
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
@@ -74,13 +85,50 @@ export class Fiber {
if (parent) {
this.deep = parent.deep;
const root = parent.root!;
root.counter++;
root.setCounter(root.counter + 1);
this.root = root;
parent.children.push(this);
} else {
this.root = this as any;
}
}
render() {
// if some parent has a fiber => register in followup
let prev = this.root!.node;
let scheduler = prev.app.scheduler;
let current = prev.parent;
while (current) {
if (current.fiber) {
let root = current.fiber.root!;
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
current = root.node;
} else {
scheduler.delayedRenders.push(this);
return;
}
}
prev = current;
current = current.parent;
}
// there are no current rendering from above => we can render
this._render();
}
_render() {
const node = this.node;
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
handleError({ node, error: e });
}
root.setCounter(root.counter - 1);
}
}
}
export class RootFiber extends Fiber {
@@ -144,6 +192,13 @@ export class RootFiber extends Fiber {
handleError({ fiber: current || this, error: e });
}
}
setCounter(newValue: number) {
this.counter = newValue;
if (newValue === 0) {
this.node.app.scheduler.flush();
}
}
}
type Position = "first-child" | "last-child";
@@ -165,6 +220,7 @@ export class MountFiber extends RootFiber {
let current: Fiber | undefined = this;
try {
const node = this.node;
node.children = this.childrenMap;
(node.app.constructor as any).validateTarget(this.target);
if (node.bdom) {
// this is a complicated situation: if we mount a fiber with an existing
+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) {
+40 -41
View File
@@ -11,27 +11,16 @@ export class Scheduler {
// interactions with other code, such as test frameworks that override them
static requestAnimationFrame = window.requestAnimationFrame.bind(window);
tasks: Set<RootFiber> = new Set();
isRunning: boolean = false;
requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0;
delayedRenders: Fiber[] = [];
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
start() {
this.isRunning = true;
this.scheduleTasks();
}
stop() {
this.isRunning = false;
}
addFiber(fiber: Fiber) {
this.tasks.add(fiber.root!);
if (!this.isRunning) {
this.start();
}
}
/**
@@ -39,39 +28,49 @@ export class Scheduler {
* Other tasks are left unchanged.
*/
flush() {
this.tasks.forEach((fiber) => {
if (fiber.root !== fiber) {
this.tasks.delete(fiber);
return;
}
const hasError = fibersInError.has(fiber);
if (hasError && fiber.counter !== 0) {
this.tasks.delete(fiber);
return;
}
if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber);
return;
}
if (fiber.counter === 0) {
if (!hasError) {
fiber.complete();
if (this.delayedRenders.length) {
let renders = this.delayedRenders;
this.delayedRenders = [];
for (let f of renders) {
if (f.root && f.node.status !== STATUS.DESTROYED) {
f.render();
}
this.tasks.delete(fiber);
}
});
if (this.tasks.size === 0) {
this.stop();
}
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => {
this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber));
for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task);
}
}
});
}
}
scheduleTasks() {
this.requestAnimationFrame(() => {
this.flush();
if (this.isRunning) {
this.scheduleTasks();
processFiber(fiber: RootFiber) {
if (fiber.root !== fiber) {
this.tasks.delete(fiber);
return;
}
const hasError = fibersInError.has(fiber);
if (hasError && fiber.counter !== 0) {
this.tasks.delete(fiber);
return;
}
if (fiber.node.status === STATUS.DESTROYED) {
this.tasks.delete(fiber);
return;
}
if (fiber.counter === 0) {
if (!hasError) {
fiber.complete();
}
});
this.tasks.delete(fiber);
}
}
}
+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());
}
// -----------------------------------------------------------------------------
+29
View File
@@ -232,6 +232,11 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
if (key === TARGET) {
return target;
}
// non-writable non-configurable properties cannot be made reactive
const desc = Object.getOwnPropertyDescriptor(target, key);
if (desc && !desc.writable && !desc.configurable) {
return Reflect.get(target, key, proxy);
}
observeTargetKey(target, key, callback);
return possiblyReactive(Reflect.get(target, key, proxy), callback);
},
@@ -308,6 +313,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 +397,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 +413,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);
+6 -4
View File
@@ -15,11 +15,13 @@ export function batched(callback: Callback): Callback {
await Promise.resolve();
if (!called) {
called = true;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
// so that only the first call to the batched function calls the original callback.
// Schedule this before calling the callback so that calls to the batched function
// within the callback will proceed only after resetting called to false, and have
// a chance to execute the callback again
Promise.resolve().then(() => (called = false));
callback();
}
};
}
+13 -1
View File
@@ -169,13 +169,25 @@ describe("properties", () => {
expect(input.value).toBe("potato");
});
test("input with value attribute, and undefined given", () => {
test("input with value attribute, and falsy value 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("");
patch(tree, block([null]));
expect(input.value).toBe("");
patch(tree, block([0]));
expect(input.value).toBe("0");
patch(tree, block([""]));
expect(input.value).toBe("");
patch(tree, block([false]));
expect(input.value).toBe("");
});
test("input type=checkbox with checked attribute", () => {
+19
View File
@@ -1370,6 +1370,25 @@ describe("qweb parser", () => {
});
});
test("a component with an empty named slot", async () => {
expect(parse(`<MyComponent><t t-set-slot="mySlot"></t></MyComponent>`)).toEqual({
dynamicProps: null,
isDynamic: false,
name: "MyComponent",
on: null,
props: null,
slots: {
mySlot: {
attrs: null,
content: null,
on: null,
scope: null,
},
},
type: 11,
});
});
test("a component with a named slot", async () => {
expect(parse(`<MyComponent><t t-set-slot="name">foo</t></MyComponent>`)).toEqual({
type: ASTType.TComponent,
@@ -52,6 +52,50 @@ exports[`Cascading renders after microtaskTick 3`] = `
}"
`;
exports[`another scenario with delayed rendering 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].value<15) {
b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`another scenario with delayed rendering 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['props'].value);
const b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`another scenario with delayed rendering 3`] = `
"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['increment'], ctx];
let txt1 = ctx['state'].val;
return block1([hdlr1, txt1]);
}
}"
`;
exports[`async rendering destroying a widget before start is over 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1067,6 +1111,287 @@ exports[`delay willUpdateProps with rendering grandchild 4`] = `
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`C\`, {value: ctx['state'].someValue+ctx['props'].value}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = component(\`D\`, {}, key + \`__1\`, node, ctx);
let txt1 = ctx['props'].value;
const b3 = block3([txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 4`] = `
"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['increment'], ctx];
let txt1 = ctx['state'].val;
return block1([hdlr1, txt1]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`B\`);
if (ctx['state'].hasChild) {
b3 = component(\`C\`, {value: ctx['state'].someValue+ctx['props'].value}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block4 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
const b3 = component(\`D\`, {}, key + \`__1\`, node, ctx);
let txt1 = ctx['props'].value;
const b4 = block4([txt1]);
return multi([b2, b3, b4]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, reusing fiber and stuff 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`delayed rendering, reusing fiber and stuff 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['props'].value);
const b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, reusing fiber and stuff 3`] = `
"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['increment'], ctx];
let txt1 = ctx['state'].val;
return block1([hdlr1, txt1]);
}
}"
`;
exports[`delayed rendering, reusing fiber then component is destroyed and stuff 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].value<15) {
b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, reusing fiber then component is destroyed and stuff 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['props'].value);
const b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, reusing fiber then component is destroyed and stuff 3`] = `
"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['increment'], ctx];
let txt1 = ctx['state'].val;
return block1([hdlr1, txt1]);
}
}"
`;
exports[`delayed rendering, then component is destroyed and stuff 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`delayed rendering, then component is destroyed and stuff 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['props'].value);
if (ctx['props'].value<10) {
b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, then component is destroyed and stuff 3`] = `
"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['increment'], ctx];
let txt1 = ctx['state'].val;
return block1([hdlr1, txt1]);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\` A \`);
if (ctx['state'].flag) {
const b4 = component(\`B\`, {value: ctx['state'].valueB}, key + \`__1\`, node, ctx);
const b5 = component(\`C\`, {value: ctx['state'].valueC}, key + \`__2\`, node, ctx);
b3 = multi([b4, b5]);
}
return multi([b2, b3]);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].value);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].val+ctx['props'].value);
}
}"
`;
exports[`destroying/recreating a subcomponent, other scenario 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1244,6 +1569,70 @@ exports[`rendering parent twice, with different props on child and stuff 2`] = `
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
const b4 = component(\`D\`, {}, key + \`__2\`, node, ctx);
return multi([b2, b3, b4]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`B\`);
if (ctx['props'].value===33) {
b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`t-foreach with dynamic async component 1`] = `
"function anonymous(bdom, helpers
) {
@@ -85,6 +85,64 @@ exports[`basics simple catchError 2`] = `
}"
`;
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['state'].value);
if (ctx['state'].hasChild) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 2`] = `
"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[`can catch errors an error in onWillDestroy, variation 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['state'].value);
if (ctx['state'].hasChild) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors an error in onWillDestroy, variation 2`] = `
"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[`can catch errors calling a hook outside setup should crash 1`] = `
"function anonymous(bdom, helpers
) {
@@ -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
) {
@@ -1,5 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`reactivity in lifecycle Child component doesn't render when state they depend on changes but their parent is about to unmount them 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2;
if (ctx['state'].renderChild) {
b2 = component(\`Child\`, {state: ctx['state']}, key + \`__1\`, node, ctx);
}
return multi([b2]);
}
}"
`;
exports[`reactivity in lifecycle Child component doesn't render when state they depend on changes but their parent is about to unmount them 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].state.content.a);
}
}"
`;
exports[`reactivity in lifecycle can use a state hook 1`] = `
"function anonymous(bdom, helpers
) {
@@ -127,6 +127,30 @@ exports[`rendering semantics props are reactive 2`] = `
}"
`;
exports[`rendering semantics render need a boolean = true to be 'deep' 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['state'].value);
const b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics render need a boolean = true to be 'deep' 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics render with deep=true followed by render with deep=false work as expected 1`] = `
"function anonymous(bdom, helpers
) {
@@ -626,7 +626,7 @@ exports[`slots dynamic t-slot call 2`] = `
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['toggle'], ctx];
const slot1 = (ctx['current'].slot);
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1), true, null);
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1, true, null));
return block1([hdlr1], [b2]);
}
}"
@@ -1444,6 +1444,110 @@ exports[`slots simple default slot, variation 2`] = `
}"
`;
exports[`slots simple dynamic slot with slot scope 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, markRaw } = helpers;
function slot1(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['slotScope'].bool) {
b2 = text(\`some text\`);
} else {
b3 = text(\`other text\`);
}
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: markRaw({'slotName': {__render: slot1, __ctx: ctx1, __scope: \\"slotScope\\"}})}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple dynamic slot with slot scope 2`] = `
"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>\`);
return function template(ctx, node, key = \\"\\") {
const slot1 = ('slotName');
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1, true, {bool: ctx['state'].bool}));
return block1([], [b2]);
}
}"
`;
exports[`slots simple named and empty slot -- 2 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, markRaw } = helpers;
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: markRaw({'myEmptySlot': {myProp: 'myProp text'}})}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple named and empty slot -- 2 2`] = `
"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>\`);
function defaultContent1(ctx, node, key = \\"\\") {
return text(\`default empty\`);
}
return function template(ctx, node, key = \\"\\") {
const b3 = callSlot(ctx, node, key, 'myEmptySlot', false, null, defaultContent1);
return block1([], [b3]);
}
}"
`;
exports[`slots simple named and empty slot 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { capture, markRaw } = helpers;
function slot1(ctx, node, key = \\"\\") {
return text(\`some text\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return component(\`Child\`, {slots: markRaw({'myEmptySlot': {}, 'default': {__render: slot1, __ctx: ctx1}})}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`slots simple named and empty slot 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<span><block-child-0/><block-child-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callSlot(ctx, node, key, 'default', false, null);
const b3 = callSlot(ctx, node, key, 'myEmptySlot', false, null);
return block1([], [b2, b3]);
}
}"
`;
exports[`slots simple slot with slot scope 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1689,7 +1793,7 @@ exports[`slots slot content has different key from other content -- dynamic slot
return function template(ctx, node, key = \\"\\") {
const b2 = component(\`Child\`, {parent: 'SlotDisplay'}, key + \`__1\`, node, ctx);
const slot1 = (ctx['slotName']);
const b3 = toggler(slot1, callSlot(ctx, node, key, slot1), true, null);
const b3 = toggler(slot1, callSlot(ctx, node, key, slot1, true, null));
return multi([b2, b3]);
}
}"
+774 -44
View File
@@ -3,6 +3,8 @@ import {
Component,
mount,
onMounted,
onRendered,
onWillDestroy,
onWillStart,
onWillUnmount,
onWillUpdateProps,
@@ -113,8 +115,8 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect(n).toBe(2);
expect([
"W:willRender",
"Child:willDestroy",
"W:willRender",
"Child:setup",
"Child:willStart",
"W:rendered",
@@ -171,6 +173,11 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
@@ -243,8 +250,8 @@ test("creating two async components, scenario 1", async () => {
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"ChildA:willDestroy",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildB:setup",
@@ -695,8 +702,8 @@ test("rendering component again in next microtick", async () => {
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Parent:willRender",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
@@ -1079,11 +1086,7 @@ test("concurrent renderings scenario 3", async () => {
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
expect([
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentC:rendered",
]).toBeLogged();
expect([]).toBeLogged();
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
@@ -1099,14 +1102,7 @@ test("concurrent renderings scenario 3", async () => {
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
expect([]).toBeLogged();
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>2d</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
expect([
"ComponentD:willRender",
"ComponentD:rendered",
@@ -1119,6 +1115,7 @@ test("concurrent renderings scenario 3", async () => {
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 4", async () => {
@@ -1207,11 +1204,7 @@ test("concurrent renderings scenario 4", async () => {
stateC.fromC = "d";
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
expect([
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentC:rendered",
]).toBeLogged();
expect([]).toBeLogged();
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
@@ -1227,6 +1220,12 @@ test("concurrent renderings scenario 4", async () => {
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>1c</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
expect([]).toBeLogged();
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>2d</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
expect([
@@ -1241,12 +1240,6 @@ test("concurrent renderings scenario 4", async () => {
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("<div><p><span><i>2d</i></span></p></div>");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
expect([]).toBeLogged();
});
test("concurrent renderings scenario 5", async () => {
@@ -1738,6 +1731,7 @@ test("concurrent renderings scenario 10", async () => {
expect(fixture.innerHTML).toBe("<div><p></p></div>");
expect([
"ComponentA:willRender",
"ComponentC:willDestroy",
"ComponentB:willUpdateProps",
"ComponentA:rendered",
]).toBeLogged();
@@ -1748,7 +1742,6 @@ test("concurrent renderings scenario 10", async () => {
expect(rendered).toBe(1);
expect([
"ComponentB:willRender",
"ComponentC:willDestroy",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentB:rendered",
@@ -2288,11 +2281,11 @@ test("concurrent renderings scenario 16", async () => {
"D:setup",
"D:willStart",
"C:rendered",
"D:willDestroy",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"D:willDestroy",
"D:setup",
"D:willStart",
"C:rendered",
@@ -2714,13 +2707,7 @@ test("delay willUpdateProps", async () => {
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("0_0");
expect([
"Child:willRender",
"Child:rendered",
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
]).toBeLogged();
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
promise = makeDeferred();
const prom2 = promise;
@@ -2837,13 +2824,9 @@ test("delay willUpdateProps with rendering grandchild", async () => {
await nextTick();
expect(fixture.innerHTML).toBe("0_0<div></div>");
expect([
"DelayedChild:willRender",
"DelayedChild:rendered",
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
"Parent:willRender",
"DelayedChild:willUpdateProps",
"ReactiveChild:willUpdateProps",
@@ -2864,8 +2847,6 @@ test("delay willUpdateProps with rendering grandchild", async () => {
"GrandParent:willRender",
"Parent:willUpdateProps",
"GrandParent:rendered",
"ReactiveChild:willRender",
"ReactiveChild:rendered",
"Parent:willRender",
"DelayedChild:willUpdateProps",
"ReactiveChild:willUpdateProps",
@@ -3015,13 +2996,13 @@ test("t-key on dom node having a component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
@@ -3073,13 +3054,13 @@ test("t-key on dynamic async component (toggler is never patched)", async () =>
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
@@ -3132,13 +3113,13 @@ test("t-foreach with dynamic async component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>");
expect([
"Child (2):willDestroy",
"Child (3):setup",
"Child (3):willStart",
"Child (3):willRender",
"Child (3):rendered",
"Child (1):willUnmount",
"Child (1):willDestroy",
"Child (2):willDestroy",
"Child (3):mounted",
]).toBeLogged();
});
@@ -3254,6 +3235,755 @@ test("rendering parent twice, with different props on child and stuff", async ()
]).toBeLogged();
});
test("delayed rendering, but then initial rendering is cancelled by yet another render", async () => {
const promC = makeDeferred();
let stateB: any = null;
class D extends Component {
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class C extends Component {
static template = xml`<D/><p><t t-esc="props.value"/></p>`;
static components = { D };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promC);
}
}
class B extends Component {
static template = xml`<C value="state.someValue + props.value"/>`;
static components = { C };
state = useState({ someValue: 3 });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class A extends Component {
static template = xml`<B value="state.value"/>`;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("<button>1</button><p>36</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"D:setup",
"D:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// update B and C, but render is blocked by C willupdateProps
stateB.someValue = 5;
await nextTick();
expect(["B:willRender", "C:willUpdateProps", "B:rendered"]).toBeLogged();
// update D => render should be delayed, because B is currently rendering
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// update A => render should go to B and cancel it
parent.state.value = 34;
await nextTick();
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
promC.resolve();
await nextTick();
expect([
"C:willRender",
"C:rendered",
"D:willRender",
"D:rendered",
"D:willPatch",
"D:patched",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"C:patched",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("<button>2</button><p>39</p>");
});
test("delayed rendering, reusing fiber and stuff", async () => {
let prom1 = makeDeferred();
let prom2 = makeDeferred();
class C extends Component {
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`<t t-esc="props.value"/><C />`;
static components = { C };
setup() {
useLogLifecycle();
let flag = false;
onWillUpdateProps(() => {
flag = true;
return prom1;
});
onRendered(async () => {
if (flag) {
await nextMicroTick();
prom2.resolve();
}
});
}
}
class A extends Component {
static template = xml`<B value="state.value"/>`;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("33<button>1</button>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 34;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C => delayed because of render in A
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// wait for render in A to be completed
prom1.resolve();
await prom2;
expect(["B:willRender", "B:rendered", "C:willRender", "C:rendered"]).toBeLogged();
// initiate a new render in A => fiber will be reused
parent.state.value = 355;
await nextTick();
expect(fixture.innerHTML).toBe("355<button>2</button>");
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"B:patched",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
});
test("delayed rendering, then component is destroyed and stuff", async () => {
let prom1 = makeDeferred();
class C extends Component {
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`<t t-esc="props.value"/><t t-if="props.value lt 10"><C /></t>`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml`<B value="state.value"/>`;
static components = { B };
state = useState({ value: 3 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("3<button>1</button>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in C (so will be first task)
fixture.querySelector("button")!.click();
// initiate a render in A, but is blocked in B. the render will destroy c. also,
// it blocks the render C
parent.state.value = 34;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// wait for render in A to be completed
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("34");
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
await nextTick();
});
test("delayed rendering, reusing fiber then component is destroyed and stuff", async () => {
let prom1 = makeDeferred();
class C extends Component {
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`<t t-esc="props.value"/><C />`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml`A<t t-if="state.value lt 15"><B value="state.value"/></t>`;
static components = { B };
state = useState({ value: 3 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("A3<button>1</button>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 5;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C (will be delayed because of render in A)
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// initiate a render in A, that will destroy B
parent.state.value = 23;
await nextTick();
expect(fixture.innerHTML).toBe("A");
expect([
"A:willRender",
"A:rendered",
"A:willPatch",
"B:willUnmount",
"C:willUnmount",
"C:willDestroy",
"B:willDestroy",
"A:patched",
]).toBeLogged();
});
test("another scenario with delayed rendering", async () => {
let prom1 = makeDeferred();
let onSecondRenderA = makeDeferred();
class C extends Component {
static template = xml`<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`<t t-esc="props.value"/><C />`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => prom1);
}
}
class A extends Component {
static template = xml`A<t t-if="state.value lt 15"><B value="state.value"/></t>`;
static components = { B };
state = useState({ value: 3 });
setup() {
useLogLifecycle();
let n = 0;
onRendered(() => {
n++;
if (n === 2) {
onSecondRenderA.resolve();
}
});
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("A3<button>1</button>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.value = 5;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// initiate a render in C (will be delayed because of render in A)
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
// initiate a render in A, that will destroy B
parent.state.value = 23;
await onSecondRenderA;
await nextMicroTick();
expect(["A:willRender", "A:rendered"]).toBeLogged();
// rerender A, but without destroying B
parent.state.value = 7;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
prom1.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("A7<button>2</button>");
expect([
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"B:patched",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
});
test("destroyed component causes other soon to be destroyed component to rerender, weird stuff happens", async () => {
let def = makeDeferred();
let c: any = null;
class B extends Component {
static template = xml`<t t-esc="props.value"/>`;
setup() {
useLogLifecycle();
onRendered(() => {
def.resolve();
});
onWillDestroy(() => {
c.state.val++;
c.render();
});
}
}
class C extends Component {
static template = xml`<t t-esc="state.val + props.value"/>`;
state = useState({ val: 0 });
setup() {
c = this;
useLogLifecycle();
}
}
class A extends Component {
static template = xml`
A
<t t-if="state.flag">
<B value="state.valueB"/>
<C value="state.valueC"/>
</t>`;
static components = { B, C };
state = useState({ flag: false, valueB: 1, valueC: 2 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe(" A ");
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.flag = true;
await def;
await nextMicroTick();
expect([
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
]).toBeLogged();
// initiate render in A => will cancel renders in B/C and restarts
parent.state.valueB = 2;
await nextTick();
expect([
"B:willDestroy",
"C:willDestroy",
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"C:mounted",
"B:mounted",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe(" A 22");
});
test("delayed rendering, destruction, stuff happens", async () => {
const promC = makeDeferred();
let stateB: any = null;
class D extends Component {
static template = xml`D<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class C extends Component {
static template = xml`C<D/><p><t t-esc="props.value"/></p>`;
static components = { D };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promC);
}
}
class B extends Component {
static template = xml`B<t t-if="state.hasChild"><C value="state.someValue + props.value"/></t>`;
static components = { C };
state = useState({ someValue: 3, hasChild: true });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class A extends Component {
static template = xml`A<B value="state.value"/>`;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABCD<button>1</button><p>36</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"D:setup",
"D:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// render in A, it updates B and C, but render is blocked in C
parent.state.value = 50;
await nextTick();
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
// update B => removes child C
stateB.hasChild = false;
// update D => render should be delayed, because AB is currently rendering
fixture.querySelector("button")!.click();
await nextTick();
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"D:willUnmount",
"D:willDestroy",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("AB");
});
test("renderings, destruction, patch, stuff, ... yet another variation", async () => {
const promB = makeDeferred();
class D extends Component {
static template = xml`D<p t-on-click="increment"><t t-esc="state.val"/></p>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
// almost the same as D
class C extends Component {
static template = xml`C<span t-on-click="increment"><t t-esc="state.val"/></span>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`B<t t-if="props.value === 33"><C/></t>`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promB);
}
}
class A extends Component {
static template = xml`A<B value="state.value"/><D/>`;
static components = { B, D };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABC<span>1</span>D<p>1</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"D:setup",
"D:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"D:willRender",
"D:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"D:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// render in A, it updates B, will remove C, stopped in B
parent.state.value = 50;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// update C => render should be delayed, because AB is currently rendering
fixture.querySelector("span")!.click();
await nextTick();
expect([]).toBeLogged();
// resolve prom B => render is done, component C is destroyed
promB.resolve();
await nextTick();
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("ABD<p>1</p>");
// update D => should just render completely independently
fixture.querySelector("p")!.click();
await nextTick();
expect(["D:willRender", "D:rendered", "D:willPatch", "D:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("ABD<p>2</p>");
});
// test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 };
+123 -1
View File
@@ -1,4 +1,4 @@
import { Component, mount } from "../../src";
import { Component, mount, onWillDestroy } from "../../src";
import {
onError,
onMounted,
@@ -1197,4 +1197,126 @@ describe("can catch errors", () => {
expect(fixture.innerHTML).toBe("<div>Child 2</div>");
expect(steps).toEqual(["Error Component"]);
});
test("an error in onWillDestroy", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
static components = { Child };
state = useState({ value: 1, hasChild: true });
setup() {
useLogLifecycle();
onError(() => {
this.state.value++;
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1<div>abc</div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.hasChild = false;
await nextTick();
await nextTick();
await nextTick();
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
test("an error in onWillDestroy, variation", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
static components = { Child };
state = useState({ value: 1, hasChild: false });
setup() {
useLogLifecycle();
onError(() => {
this.state.value++;
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
parent.state.hasChild = true;
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
parent.state.hasChild = false;
await nextTick();
expect([
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
});
+30
View File
@@ -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 {
+45 -1
View File
@@ -8,7 +8,7 @@ import {
useState,
xml,
} from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
@@ -155,4 +155,48 @@ describe("reactivity in lifecycle", () => {
expect(steps).toEqual([2]);
expect(fixture.innerHTML).toBe("<div>2</div>");
});
test("Child component doesn't render when state they depend on changes but their parent is about to unmount them", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.state.content.a"/>`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`<Child t-if="state.renderChild" state="state"/>`;
static components = { Child };
state: any = useState({ renderChild: true, content: { a: 2 } });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("2");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.content = null;
parent.state.renderChild = false;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
});
});
+37
View File
@@ -102,6 +102,43 @@ describe("rendering semantics", () => {
expect(childN).toBe(2);
});
test("render need a boolean = true to be 'deep'", async () => {
let childN = 0;
let parentN = 0;
class Child extends Component {
static template = xml`child`;
setup() {
onRendered(() => childN++);
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<Child/>
`;
static components = { Child };
state = { value: "A" };
setup() {
onRendered(() => parentN++);
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect(parentN).toBe(1);
expect(childN).toBe(1);
parent.state.value = "B";
parent.render("true" as any as boolean);
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect(parentN).toBe(2);
expect(childN).toBe(1);
});
test("render with deep=true followed by render with deep=false work as expected", async () => {
class Child extends Component {
static template = xml`child<t t-esc="env.getValue()"/>`;
+66
View File
@@ -74,6 +74,72 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("<span>other text</span>");
});
test("simple dynamic slot with slot scope", async () => {
let child: any;
class Child extends Component {
static template = xml`<span><t t-slot="{{ 'slotName' }}" bool="state.bool"/></span>`;
state = useState({ bool: true });
setup() {
child = this;
}
}
class Parent extends Component {
static template = xml`
<Child>
<t t-set-slot="slotName" t-slot-scope="slotScope">
<t t-if="slotScope.bool">some text</t>
<t t-else="slotScope.bool">other text</t>
</t>
</Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>some text</span>");
child.state.bool = false;
await nextTick();
expect(fixture.innerHTML).toBe("<span>other text</span>");
});
test("simple named and empty slot", async () => {
class Child extends Component {
static template = xml`<span><t t-slot="default" /><t t-slot="myEmptySlot"/></span>`;
setup() {
expect(this.props.slots["myEmptySlot"]).toBeTruthy();
}
}
class Parent extends Component {
static template = xml`<Child>some text<t t-set-slot="myEmptySlot" /></Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>some text</span>");
});
test("simple named and empty slot -- 2", async () => {
class Child extends Component {
static template = xml`<span><t t-slot="myEmptySlot">default empty</t></span>`;
setup() {
expect(this.props.slots["myEmptySlot"]).toBeTruthy();
expect(this.props.slots["myEmptySlot"].myProp).toBe("myProp text");
}
}
class Parent extends Component {
static template = xml`<Child><t t-set-slot="myEmptySlot" myProp="'myProp text'" /></Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>default empty</span>");
});
test("default slot with slot scope: shorthand syntax", async () => {
let child: any;
class Child extends Component {
+1 -1
View File
@@ -49,7 +49,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 {
+104
View File
@@ -208,6 +208,46 @@ describe("Reactivity", () => {
expect(n).toBe(1); // two operations but only one notification
});
test("batched: modifying the reactive in the callback doesn't break reactivity", async () => {
let n = 0;
let obj = { a: 1 };
const state = createReactive(
obj,
batched(() => {
state.a; // subscribe to a
state.a = 2;
n++;
})
);
expect(n).toBe(0);
state.a = 2;
expect(n).toBe(0);
await nextMicroTick();
expect(n).toBe(0); // key has not be read yet
state.a = state.a + 5; // key is read and then modified
expect(n).toBe(0);
await nextMicroTick();
expect(n).toBe(1);
// the write a = 2 inside the batched callback triggered another notification, wait for it
await nextMicroTick();
expect(n).toBe(2);
// Should now be stable as we're writing the same value again
await nextMicroTick();
expect(n).toBe(2);
// Do it again to check it's not broken
state.a = state.a + 5; // key is read and then modified
expect(n).toBe(2);
await nextMicroTick();
expect(n).toBe(3);
// the write a = 2 inside the batched callback triggered another notification, wait for it
await nextMicroTick();
expect(n).toBe(4);
// Should now be stable as we're writing the same value again
await nextMicroTick();
expect(n).toBe(4);
});
test("setting property to same value does not trigger callback", async () => {
let n = 0;
const state = createReactive({ a: 1 }, () => n++);
@@ -1095,6 +1135,13 @@ describe("Reactivity", () => {
expect(n).toBe(1);
expect(state.k).toEqual({ n: 2 });
});
test("can access properties on reactive of frozen objects", async () => {
const obj = Object.freeze({ a: {} });
const state = createReactive(obj);
expect(() => state.a).not.toThrow();
expect(state.a).toBe(obj.a);
});
});
describe("Collections", () => {
@@ -1245,6 +1292,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 +1541,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", () => {
+20
View File
@@ -50,4 +50,24 @@ describe("batched", () => {
await nextMicroTick();
expect(n).toBe(1);
});
test("calling batched function from within the callback is not treated as part of the original batch", async () => {
let n = 0;
let fn = batched(() => {
n++;
if (n === 1) {
fn();
}
});
expect(n).toBe(0);
fn();
expect(n).toBe(0);
await nextMicroTick(); // First batch
expect(n).toBe(1);
await nextMicroTick(); // Second batch initiated from within the callback
expect(n).toBe(2);
await nextMicroTick();
expect(n).toBe(2);
});
});