Compare commits

..

12 Commits

Author SHA1 Message Date
Samuel Degueldre 0cd66c8518 [REL] v2.0.0-beta-7
# 2.0.0-beta-7

- fix: concurrency: do not render delayed fibers when cancelled
- imp: allow duplicate templates if and only if they are the same
2022-04-27 11:08:25 +02:00
Samuel Degueldre 5d9cff0331 [IMP] app: allow duplicate templates iff they are the same
Previously, we used an option to allow duplicate templates, if that
option was passed, we would always replace the existing template with
the new template, and if it wasn't, we would always throw an error even
if the template's content was the same.

Allowing to replace a template with a different one is problematic, as
it may or may not have already been compiled, which may cause various
problems. On the other hand, if the template is the same, there is no
point in throwing an error, as we can just silently ignore the second
addition.
2022-04-27 10:40:19 +02:00
Samuel Degueldre 41f1262eb7 [FIX] concurrency: do not render delayed fibers when cancelled
Previously, if a fiber was delayed because one of its ancestors was
rendering, and that fiber was not a root fiber, it would be rendered
after all its ancestors had finished rendering even if one of those
ancestor renderings cancelled it.

This commit fixes that by simply checking that a delayed fiber is still
its component node's current fiber before rendering it.
2022-04-19 12:07:35 +02:00
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
21 changed files with 1035 additions and 178 deletions
+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-4",
"version": "2.0.0-beta-7",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+1 -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) {
+11 -9
View File
@@ -79,18 +79,20 @@ export class TemplateSet {
this.helpers = makeHelpers(this.getTemplate.bind(this));
}
addTemplate(
name: string,
template: string | Element,
options: { allowDuplicate?: boolean } = {}
) {
if (name in this.rawTemplates && !options.allowDuplicate) {
throw new Error(`Template ${name} already defined`);
addTemplate(name: string, template: string | Element) {
if (name in this.rawTemplates) {
const rawTemplate = this.rawTemplates[name];
const currentAsString = typeof rawTemplate === "string" ? rawTemplate : rawTemplate.outerHTML;
const newAsString = typeof template === "string" ? template : template.outerHTML;
if (currentAsString === newAsString) {
return;
}
throw new Error(`Template ${name} already defined with different content`);
}
this.rawTemplates[name] = template;
}
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
addTemplates(xml: string | Document) {
if (!xml) {
// empty string
return;
@@ -98,7 +100,7 @@ export class TemplateSet {
xml = xml instanceof Document ? xml : parseXML(xml);
for (const template of xml.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name")!;
this.addTemplate(name, template, options);
this.addTemplate(name, template);
}
}
+1 -1
View File
@@ -1234,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})`;
}
+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);
}
}
+29 -36
View File
@@ -47,7 +47,7 @@ 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));
@@ -79,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;
@@ -114,12 +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(makeChildFiber(node, parentFiber));
node.initiateRender(new Fiber(node, parentFiber));
}
parentFiber.root!.reachedChildren.add(node);
parentFiber.childrenMap[key] = node;
return node;
}
@@ -137,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;
@@ -153,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;
@@ -191,9 +193,9 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
}
}
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;
@@ -215,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) {
@@ -254,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;
}
@@ -326,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;
}
@@ -334,7 +344,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
}
patch() {
debugger
if (this.fiber && this.fiber.parent) {
// we only patch here renderings coming from above. renderings initiated
// by the component will be patched independently in the appropriate
@@ -344,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;
}
@@ -360,20 +367,6 @@ 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
// ---------------------------------------------------------------------------
+5 -9
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,23 +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) {
const root = fiber.root!;
root.setCounter(root.counter - 1);
}
if (handled) {
return true;
}
}
@@ -55,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 {
+50 -82
View File
@@ -1,6 +1,6 @@
import { BDom, mount } from "../blockdom";
import type { ComponentNode } from "./component_node";
import { handleError } from "./error_handling";
import { fibersInError, handleError } from "./error_handling";
import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
@@ -9,72 +9,30 @@ export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
cancelFibers(current.children);
current.root = null;
}
let fiber = new Fiber(node);
fiber.deep = parent.deep;
const root = parent.root!;
fiber.root = root;
parent.children.push(fiber);
fiber.parent = parent;
root.setCounter(root.counter + 1);
return fiber;
return new Fiber(node, parent);
}
export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
debugger;
let parent = current.parent;
let n = cancelFibers(current.children);
current.root = null;
// current.bdom = null;
if (parent) {
let fiber = new Fiber(node);
fiber.deep = parent.deep;
const root = parent.root!;
fiber.root = root;
let index = parent.children.indexOf(current);
parent.children[index] = fiber;
// parent.children.push(fiber);
fiber.parent = parent;
// node.fiber =
root.setCounter(root.counter + 1 - n);
if (node.willPatch.length) {
let index = root.willPatch.indexOf(current);
if (index >= 0) {
root.willPatch[index] = fiber;
}
}
if (node.patched.length) {
let index = root.patched.indexOf(current);
if (index >= 0) {
root.patched[index] = fiber;
}
}
return fiber;
let root = current.root!;
// 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);
fibersInError.delete(root);
current.appliedToDom = false;
}
// let parent = current.parent;
// let fiber = new Fiber(node);
// return fiber;
// cancelFibers(current.children);
// current.root = null;
// let root = current.root!;
// root.setCounter(root.counter + 1 - cancelFibers(current.children));
// current.children = [];
// current.bdom = null;
// if (current === root) {
// root.reachedChildren = new WeakSet();
// }
// if (fibersInError.has(current)) {
// fibersInError.delete(current);
// fibersInError.delete(root);
// current.appliedToDom = false;
// }
// return current;
return current;
}
const fiber = new RootFiber(node);
fiber.root = fiber;
const fiber = new RootFiber(node, null);
if (node.willPatch.length) {
fiber.willPatch.push(fiber);
}
@@ -84,13 +42,22 @@ export function makeRootFiber(node: ComponentNode): Fiber {
return fiber;
}
function throwOnRender() {
throw new Error("Attempted to render cancelled fiber");
}
/**
* @returns number of not-yet rendered fibers cancelled
*/
function cancelFibers(fibers: Fiber[]): number {
let result = 0;
for (let fiber of fibers) {
fiber.node.fiber = null;
let node = fiber.node;
fiber.render = throwOnRender;
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
@@ -98,7 +65,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++;
}
@@ -107,20 +74,28 @@ function cancelFibers(fibers: Fiber[]): number {
return result;
}
(window as any).fibers = [];
export class Fiber {
node: ComponentNode;
bdom: BDom | null = null;
root: RootFiber | null = null; // A Fiber that has been replaced by another has no root
parent: Fiber | null = null;
root: RootFiber | null; // A Fiber that has been replaced by another has no root
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
deep: boolean = false;
childrenMap: ComponentNode["children"] = {};
constructor(node: ComponentNode) {
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
(window as any).fibers.push(this);
this.parent = parent;
if (parent) {
this.deep = parent.deep;
const root = parent.root!;
root.setCounter(root.counter + 1);
this.root = root;
parent.children.push(this);
} else {
this.root = this as any;
}
}
render() {
@@ -131,16 +106,11 @@ export class Fiber {
while (current) {
if (current.fiber) {
let root = current.fiber.root!;
if (root.counter) {
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
current = root.node;
} else {
scheduler.delayedRenders.push(this);
return;
} else {
if (!root.reachedChildren.has(prev)) {
// is dead. but we keep the render around just in case
scheduler.delayedRenders.push(this);
return;
}
current = root.node;
}
}
prev = current;
@@ -156,11 +126,12 @@ export class Fiber {
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
root.setCounter(root.counter - 1);
} catch (e) {
handleError({ node, error: e });
}
root.setCounter(root.counter - 1);
}
}
}
@@ -176,8 +147,6 @@ export class RootFiber extends Fiber {
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed
locked: boolean = false;
reachedChildren: WeakSet<ComponentNode> = new WeakSet();
complete() {
const node = this.node;
this.locked = true;
@@ -230,7 +199,6 @@ export class RootFiber extends Fiber {
}
setCounter(newValue: number) {
debugger;
this.counter = newValue;
if (newValue === 0) {
this.node.app.scheduler.flush();
@@ -249,15 +217,15 @@ export class MountFiber extends RootFiber {
position: Position;
constructor(node: ComponentNode, target: HTMLElement, options: MountOptions = {}) {
super(node);
super(node, null);
this.target = target;
this.root = this;
this.position = options.position || "last-child";
}
complete() {
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
+1 -1
View File
@@ -32,7 +32,7 @@ export class Scheduler {
let renders = this.delayedRenders;
this.delayedRenders = [];
for (let f of renders) {
if (f.root) {
if (f.root && f.node.status !== STATUS.DESTROYED && f.node.fiber === f) {
f.render();
}
}
-9
View File
@@ -12,15 +12,6 @@ describe("error handling", () => {
expect(() => context.renderToString("invalidname")).toThrow("Missing template");
});
test("cannot add twice the same template", () => {
const context = new TestContext();
context.addTemplate("test", `<t></t>`);
expect(() => context.addTemplate("test", "<div/>", { allowDuplicate: true })).not.toThrow(
"already defined"
);
expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined");
});
test("addTemplates throw if parser error", () => {
const context = new TestContext();
expect(() => {
+5 -4
View File
@@ -11,11 +11,12 @@ describe("basic validation", () => {
expect(() => context.getTemplate("invalidname")).toThrow("Missing template");
});
test("cannot add twice the same template", () => {
test("cannot add a different template with the same name", () => {
const context = new TemplateSet();
expect(() => context.addTemplate("test", "<div/>", { allowDuplicate: true })).not.toThrow(
"already defined"
);
context.addTemplate("test", `<t/>`);
// Same template with the same name is fine
expect(() => context.addTemplate("test", "<t/>")).not.toThrow();
// Different template with the same name crashes
expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined");
});
@@ -1111,6 +1111,56 @@ exports[`delay willUpdateProps with rendering grandchild 4`] = `
}"
`;
exports[`delayed fiber does not get rendered if it was cancelled 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\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed fiber does not get rendered if it was cancelled 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed fiber does not get rendered if it was cancelled 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
const b3 = component(\`D\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed fiber does not get rendered if it was cancelled 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`D\`);
}
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 1`] = `
"function anonymous(bdom, helpers
) {
@@ -1164,6 +1214,69 @@ exports[`delayed rendering, but then initial rendering is cancelled by yet anoth
}"
`;
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
) {
@@ -1289,6 +1402,46 @@ exports[`delayed rendering, then component is destroyed and stuff 3`] = `
}"
`;
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
) {
@@ -1466,6 +1619,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
) {
@@ -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,45 @@ 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
) {
@@ -1754,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]);
}
}"
+386 -11
View File
@@ -4,6 +4,7 @@ import {
mount,
onMounted,
onRendered,
onWillDestroy,
onWillStart,
onWillUnmount,
onWillUpdateProps,
@@ -114,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",
@@ -172,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",
@@ -244,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",
@@ -696,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",
@@ -870,7 +876,6 @@ test("concurrent renderings scenario 2", async () => {
"ComponentB:rendered",
]).toBeLogged();
debugger;
stateB.fromB = "c";
await nextTick();
expect(fixture.innerHTML).toBe("<div>1<p><span>1b</span></p></div>");
@@ -882,7 +887,7 @@ test("concurrent renderings scenario 2", async () => {
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
debugger
expect(fixture.innerHTML).toBe("<div>2<p><span>2c</span></p></div>");
expect([
"ComponentC:willRender",
"ComponentC:rendered",
@@ -893,7 +898,6 @@ test("concurrent renderings scenario 2", async () => {
"ComponentB:patched",
"ComponentA:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("<div>2<p><span>2c</span></p></div>");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
@@ -1727,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();
@@ -1737,7 +1742,6 @@ test("concurrent renderings scenario 10", async () => {
expect(rendered).toBe(1);
expect([
"ComponentB:willRender",
"ComponentC:willDestroy",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentB:rendered",
@@ -2277,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",
@@ -2992,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();
});
@@ -3050,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();
});
@@ -3109,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();
});
@@ -3692,6 +3696,377 @@ test("another scenario with delayed rendering", async () => {
]).toBeLogged();
});
test("delayed fiber does not get rendered if it was cancelled", async () => {
class D extends Component {
static template = xml`D`;
setup() {
useLogLifecycle("", true);
}
}
class C extends Component {
static template = xml`C<D/>`;
static components = { D };
setup() {
useLogLifecycle("", true);
c = this;
}
}
let c: C;
class B extends Component {
static template = xml`B<C/>`;
static components = { C };
setup() {
useLogLifecycle("", true);
}
}
class A extends Component {
static template = xml`A<B/>`;
static components = { B };
setup() {
useLogLifecycle("", true);
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABCD");
expect([
"A:setup",
"A:willRender",
"B:setup",
"A:rendered",
"B:willRender",
"C:setup",
"B:rendered",
"C:willRender",
"D:setup",
"C:rendered",
"D:willRender",
"D:rendered",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// Start a render in C
c!.render(true);
await nextMicroTick();
expect(["C:willRender", "C:rendered"]).toBeLogged();
// Start a render in A such that C is already rendered, but D will be delayed
// (because A is rendering) then cancelled (when the render from A reaches C)
a.render(true);
// Make sure the render can go to completion (Cancelled fibers will throw when rendered)
await nextTick();
expect([
"A:willRender",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"D:willRender",
"D:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"D:willPatch",
"D:patched",
"C:patched",
"B:patched",
"A: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");
});
});
+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()"/>`;
+29
View File
@@ -74,6 +74,35 @@ 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>`;
+13 -9
View File
@@ -138,7 +138,7 @@ const steps: string[] = [];
export function logStep(step: string) {
steps.push(step);
}
export function useLogLifecycle(key?: string) {
export function useLogLifecycle(key?: string, skipAsyncHooks: boolean = false) {
const component = useComponent();
let name = component.constructor.name;
if (key) {
@@ -147,20 +147,24 @@ export function useLogLifecycle(key?: string) {
logStep(`${name}:setup`);
expect(name + ": " + status(component)).toBe(name + ": " + "new");
onWillStart(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "new");
logStep(`${name}:willStart`);
});
if (!skipAsyncHooks) {
onWillStart(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "new");
logStep(`${name}:willStart`);
});
}
onMounted(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:mounted`);
});
onWillUpdateProps(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:willUpdateProps`);
});
if (!skipAsyncHooks) {
onWillUpdateProps(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:willUpdateProps`);
});
}
onWillRender(() => {
logStep(`${name}:willRender`);