Compare commits

..

7 Commits

Author SHA1 Message Date
Géry Debongnie 6c72e0a143 [REL] v2.0.0-beta-12
# v2.0.0-beta-12

- fix: compiler: properly handle t-set in t-if with no content
2022-06-29 11:12:59 +02:00
Géry Debongnie 382e3e4010 [FIX] compiler: properly handle t-set in t-if with no content
Before this commit, whenever Owl encounter a t-if, it generates an
anchor (a "hole") in the current block being compiled. However, in some
cases, the content of the t-if may not have any content at all, and the
anchor is then useless. Worse, the code generating the anchor generates
an index based on the number of sub blocks, but if there is no content,
the next anchor being created will have the same index, which then may
cause weird bugs.

A possible way to fix this could be to make sure we increment properly
the anchor index, but we could even do better: not having an anchor at
all.
2022-06-29 11:08:14 +02:00
Géry Debongnie 76c389a7a8 [REL] v2.0.0-beta-11
# v2.0.0-beta-11

Yet another release with some small fixes.

[FIX] fix some issues with t-out with falsy values, and with default values
[REF] app: slightly simplify the create component path
[IMP] compiler: add support for binary operators
[IMP] add support for t-call-context directive
[FIX] properly get component reference instead of context
[FIX] blockdom: fix crash when class object key has leading spaces
2022-06-28 15:28:12 +02:00
Géry Debongnie b9ba0abf41 [FIX] test: run prettier 2022-06-28 15:26:55 +02:00
Géry Debongnie 4ca37be7f3 [FIX] tests: update wrong snapshot
oups
2022-06-28 15:20:53 +02:00
Géry Debongnie d6667ddf2e [FIX] fix some issues with t-out with falsy values, and with default value 2022-06-28 15:10:11 +02:00
Géry Debongnie 6f86beeaf3 [REF] app: slightly simplify the create component path 2022-06-28 13:11:57 +02:00
26 changed files with 541 additions and 309 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.0-beta-10", "version": "2.0.0-beta-12",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
+36 -32
View File
@@ -143,7 +143,7 @@ interface Context {
ctxVar?: string; ctxVar?: string;
} }
function createContext(parentCtx: Context, params?: Partial<Context>) { function createContext(parentCtx: Context, params?: Partial<Context>): Context {
return Object.assign( return Object.assign(
{ {
block: null, block: null,
@@ -334,8 +334,8 @@ export class CodeGenerator {
this.addLine(`const ${varName} = ${expr};`); this.addLine(`const ${varName} = ${expr};`);
} }
insertAnchor(block: BlockDescription) { insertAnchor(block: BlockDescription, index: number = block.children.length) {
const tag = `block-child-${block.children.length}`; const tag = `block-child-${index}`;
const anchor = xmlDoc.createElement(tag); const anchor = xmlDoc.createElement(tag);
block.insert(anchor); block.insert(anchor);
} }
@@ -692,7 +692,7 @@ export class CodeGenerator {
const children = ast.content; const children = ast.content;
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = ast.content[i]; const child = ast.content[i];
const subCtx: Context = createContext(ctx, { const subCtx = createContext(ctx, {
block, block,
index: block!.childNumber, index: block!.childNumber,
forceNewBlock: false, forceNewBlock: false,
@@ -754,21 +754,37 @@ export class CodeGenerator {
this.insertAnchor(block); this.insertAnchor(block);
} }
block = this.createBlock(block, "html", ctx); block = this.createBlock(block, "html", ctx);
this.helpers.add(ast.expr === "0" ? "zero" : "safeOutput"); let blockStr;
let expr = ast.expr === "0" ? "ctx[zero]" : `safeOutput(${compileExpr(ast.expr)})`; if (ast.expr === "0") {
if (ast.body) { this.helpers.add("zero");
const nextId = BlockDescription.nextBlockId; blockStr = `ctx[zero]`;
const subCtx: Context = createContext(ctx); } else if (ast.body) {
let bodyValue = null;
bodyValue = BlockDescription.nextBlockId;
const subCtx = createContext(ctx);
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
this.helpers.add("withDefault"); this.helpers.add("safeOutput");
expr = `withDefault(${expr}, b${nextId})`; blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
} else {
this.helpers.add("safeOutput");
blockStr = `safeOutput(${compileExpr(ast.expr)})`;
} }
this.insertBlock(`${expr}`, block, ctx); this.insertBlock(blockStr, block, ctx);
}
compileTIfBranch(content: AST, block: BlockDescription, ctx: Context) {
this.target.indentLevel++;
let childN = block.children.length;
this.compileAST(content, createContext(ctx, { block, index: ctx.index }));
if (block.children.length > childN) {
// we have some content => need to insert an anchor at correct index
this.insertAnchor(block!, childN);
}
this.target.indentLevel--;
} }
compileTIf(ast: ASTTif, ctx: Context, nextNode?: ASTDomNode) { compileTIf(ast: ASTTif, ctx: Context, nextNode?: ASTDomNode) {
let { block, forceNewBlock, index } = ctx; let { block, forceNewBlock } = ctx;
let currentIndex = index;
const codeIdx = this.target.code.length; const codeIdx = this.target.code.length;
const isNewBlock = !block || (block.type !== "multi" && forceNewBlock); const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
if (block) { if (block) {
@@ -778,28 +794,16 @@ export class CodeGenerator {
block = this.createBlock(block, "multi", ctx); block = this.createBlock(block, "multi", ctx);
} }
this.addLine(`if (${compileExpr(ast.condition)}) {`); this.addLine(`if (${compileExpr(ast.condition)}) {`);
this.target.indentLevel++; this.compileTIfBranch(ast.content, block, ctx);
this.insertAnchor(block!);
const subCtx: Context = createContext(ctx, { block, index: currentIndex });
this.compileAST(ast.content, subCtx);
this.target.indentLevel--;
if (ast.tElif) { if (ast.tElif) {
for (let clause of ast.tElif) { for (let clause of ast.tElif) {
this.addLine(`} else if (${compileExpr(clause.condition)}) {`); this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
this.target.indentLevel++; this.compileTIfBranch(clause.content, block, ctx);
this.insertAnchor(block);
const subCtx: Context = createContext(ctx, { block, index: currentIndex });
this.compileAST(clause.content, subCtx);
this.target.indentLevel--;
} }
} }
if (ast.tElse) { if (ast.tElse) {
this.addLine(`} else {`); this.addLine(`} else {`);
this.target.indentLevel++; this.compileTIfBranch(ast.tElse, block, ctx);
this.insertAnchor(block);
const subCtx: Context = createContext(ctx, { block, index: currentIndex });
this.compileAST(ast.tElse, subCtx);
this.target.indentLevel--;
} }
this.addLine("}"); this.addLine("}");
if (isNewBlock) { if (isNewBlock) {
@@ -885,7 +889,7 @@ export class CodeGenerator {
this.addLine("}"); this.addLine("}");
} }
const subCtx: Context = createContext(ctx, { block, index: loopVar }); const subCtx = createContext(ctx, { block, index: loopVar });
this.compileAST(ast.body, subCtx); this.compileAST(ast.body, subCtx);
if (ast.memo) { if (ast.memo) {
this.addLine( this.addLine(
@@ -932,7 +936,7 @@ export class CodeGenerator {
for (let i = 0, l = ast.content.length; i < l; i++) { for (let i = 0, l = ast.content.length; i < l; i++) {
const child = ast.content[i]; const child = ast.content[i];
const isTSet = child.type === ASTType.TSet; const isTSet = child.type === ASTType.TSet;
const subCtx: Context = createContext(ctx, { const subCtx = createContext(ctx, {
block, block,
index, index,
forceNewBlock: !isTSet, forceNewBlock: !isTSet,
@@ -978,7 +982,7 @@ export class CodeGenerator {
this.addLine(`${ctxVar}[isBoundary] = 1;`); this.addLine(`${ctxVar}[isBoundary] = 1;`);
this.helpers.add("isBoundary"); this.helpers.add("isBoundary");
const nextId = BlockDescription.nextBlockId; const nextId = BlockDescription.nextBlockId;
const subCtx: Context = createContext(ctx, { preventRoot: true, ctxVar }); const subCtx = createContext(ctx, { preventRoot: true, ctxVar });
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
if (nextId !== BlockDescription.nextBlockId) { if (nextId !== BlockDescription.nextBlockId) {
this.helpers.add("zero"); this.helpers.add("zero");
+39 -55
View File
@@ -12,7 +12,7 @@ import {
TARGET, TARGET,
} from "./reactivity"; } from "./reactivity";
import { STATUS } from "./status"; import { STATUS } from "./status";
import { Callback } from "./utils"; import { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null; let currentNode: ComponentNode | null = null;
@@ -41,7 +41,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
// Integration with reactivity system (useState) // Integration with reactivity system (useState)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const renderFunctions = new WeakMap<ComponentNode, Callback>(); const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/** /**
* Creates a reactive object that will be observed by the current component. * Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the * Reading data from the returned object (eg during rendering) will cause the
@@ -54,10 +54,10 @@ const renderFunctions = new WeakMap<ComponentNode, Callback>();
*/ */
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> { export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
const node = getCurrent(); const node = getCurrent();
let render = renderFunctions.get(node)!; let render = batchedRenderFunctions.get(node)!;
if (!render) { if (!render) {
render = node.render.bind(node, false); render = batched(node.render.bind(node, false));
renderFunctions.set(node, render); batchedRenderFunctions.set(node, render);
// manual implementation of onWillDestroy to break cyclic dependency // manual implementation of onWillDestroy to break cyclic dependency
node.willDestroy.push(clearReactivesForCallback.bind(null, render)); node.willDestroy.push(clearReactivesForCallback.bind(null, render));
} }
@@ -140,22 +140,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
fiber.root!.mounted.push(fiber); fiber.root!.mounted.push(fiber);
} }
const component = this.component; const component = this.component;
if (this.willStart.length) { try {
let willStartResults; await Promise.all(this.willStart.map((f) => f.call(component)));
try { } catch (e) {
willStartResults = this.willStart.map((f) => f.call(component)); handleError({ node: this, error: e });
} catch (e) { return;
handleError({ node: this, error: e });
return;
}
if (willStartResults.some((result) => result instanceof Promise)) {
try {
await Promise.all(willStartResults);
} catch (e) {
handleError({ node: this, error: e });
return;
}
}
} }
if (this.status === STATUS.NEW && this.fiber === fiber) { if (this.status === STATUS.NEW && this.fiber === fiber) {
fiber.render(); fiber.render();
@@ -164,9 +153,10 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
async render(deep: boolean) { async render(deep: boolean) {
let current = this.fiber; let current = this.fiber;
if (current && (current.root!.locked || current as any).bdom === true) { if (current && (current.root!.locked || (current as any).bdom === true)) {
this.app.scheduler.requestAnimationFrame(() => this.render(deep)); await Promise.resolve();
return; // situation may have changed after the microtask tick
current = this.fiber;
} }
if (current) { if (current) {
if (!current.bdom && !fibersInError.has(current)) { if (!current.bdom && !fibersInError.has(current)) {
@@ -185,28 +175,26 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
const fiber = makeRootFiber(this); const fiber = makeRootFiber(this);
fiber.deep = deep; fiber.deep = deep;
this.fiber = fiber; this.fiber = fiber;
const scheduler = this.app.scheduler;
scheduler.requestAnimationFrame(() => { this.app.scheduler.addFiber(fiber);
if (this.status === STATUS.DESTROYED) { await Promise.resolve();
return; if (this.status === STATUS.DESTROYED) {
} return;
// We only want to actually render the component if the following two }
// conditions are true: // We only want to actually render the component if the following two
// * this.fiber: it could be null, in which case the render has been cancelled // conditions are true:
// * (current || !fiber.parent): if current is not null, this means that the // * this.fiber: it could be null, in which case the render has been cancelled
// render function was called when a render was already occurring. In this // * (current || !fiber.parent): if current is not null, this means that the
// case, the pending rendering was cancelled, and the fiber needs to be // render function was called when a render was already occurring. In this
// rendered to complete the work. If current is null, we check that the // case, the pending rendering was cancelled, and the fiber needs to be
// fiber has no parent. If that is the case, the fiber was downgraded from // rendered to complete the work. If current is null, we check that the
// a root fiber to a child fiber in the previous microtick, because it was // fiber has no parent. If that is the case, the fiber was downgraded from
// embedded in a rendering coming from above, so the fiber will be rendered // a root fiber to a child fiber in the previous microtick, because it was
// in the next microtick anyway, so we should not render it again. // embedded in a rendering coming from above, so the fiber will be rendered
if (this.fiber === fiber && (current || !fiber.parent)) { // in the next microtick anyway, so we should not render it again.
fiber.render(); if (this.fiber === fiber && (current || !fiber.parent)) {
} fiber.render();
}); }
scheduler.addFiber(fiber);
} }
destroy() { destroy() {
@@ -259,17 +247,14 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
} }
currentNode = null; currentNode = null;
if (this.willUpdateProps.length) { const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
const willUpdatePropsResults = this.willUpdateProps.map((f) => f.call(component, props)); await prom;
if (willUpdatePropsResults.some((res) => res instanceof Promise)) { if (fiber !== this.fiber) {
await Promise.all(willUpdatePropsResults); return;
}
if (fiber !== this.fiber) {
return;
}
} }
component.props = props; component.props = props;
this.props = rawProps; this.props = rawProps;
fiber.render();
const parentRoot = parentFiber.root!; const parentRoot = parentFiber.root!;
if (this.willPatch.length) { if (this.willPatch.length) {
parentRoot.willPatch.push(fiber); parentRoot.willPatch.push(fiber);
@@ -277,7 +262,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
if (this.patched.length) { if (this.patched.length) {
parentRoot.patched.push(fiber); parentRoot.patched.push(fiber);
} }
fiber.render();
} }
/** /**
@@ -365,7 +349,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
get subscriptions(): ReturnType<typeof getSubscriptions> { get subscriptions(): ReturnType<typeof getSubscriptions> {
const render = renderFunctions.get(this); const render = batchedRenderFunctions.get(this);
return render ? getSubscriptions(render) : []; return render ? getSubscriptions(render) : [];
} }
} }
-3
View File
@@ -20,7 +20,6 @@ export function makeRootFiber(node: ComponentNode): Fiber {
// which means any arbitrary code can be run in onWillDestroy, which may // which means any arbitrary code can be run in onWillDestroy, which may
// trigger new renderings // trigger new renderings
root.locked = true; root.locked = true;
debugger
root.setCounter(root.counter + 1 - cancelFibers(current.children)); root.setCounter(root.counter + 1 - cancelFibers(current.children));
root.locked = false; root.locked = false;
current.children = []; current.children = [];
@@ -56,7 +55,6 @@ function cancelFibers(fibers: Fiber[]): number {
let node = fiber.node; let node = fiber.node;
fiber.render = throwOnRender; fiber.render = throwOnRender;
if (node.status === STATUS.NEW) { if (node.status === STATUS.NEW) {
console.log('asdfasdfsdf')
node.destroy(); node.destroy();
delete node.parent!.children[node.parentKey!]; delete node.parent!.children[node.parentKey!];
} }
@@ -153,7 +151,6 @@ export class RootFiber extends Fiber {
complete() { complete() {
const node = this.node; const node = this.node;
this.locked = true; this.locked = true;
debugger
let current: Fiber | undefined = undefined; let current: Fiber | undefined = undefined;
try { try {
// Step 1: calling all willPatch lifecycle hooks // Step 1: calling all willPatch lifecycle hooks
+13 -30
View File
@@ -11,29 +11,12 @@ export class Scheduler {
// interactions with other code, such as test frameworks that override them // interactions with other code, such as test frameworks that override them
static requestAnimationFrame = window.requestAnimationFrame.bind(window); static requestAnimationFrame = window.requestAnimationFrame.bind(window);
tasks: Set<RootFiber> = new Set(); tasks: Set<RootFiber> = new Set();
_requestAnimationFrame: Window["requestAnimationFrame"]; requestAnimationFrame: Window["requestAnimationFrame"];
cbs: Function[] = [];
frame: number = 0; frame: number = 0;
delayedRenders: Fiber[] = []; delayedRenders: Fiber[] = [];
shouldFlush = false;
constructor() { constructor() {
this._requestAnimationFrame = Scheduler.requestAnimationFrame; this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
requestAnimationFrame(cb: Function) {
this.cbs.push(cb);
if (this.frame === 0) {
this.frame = this._requestAnimationFrame(() => {
// note that some callbacks may be added to this.cbs while this look is
// running, and they will be executed immediately
for (let i = 0; i < this.cbs.length; i++) {
this.cbs[i]();
}
this.cbs = [];
this.frame = 0;
});
}
} }
addFiber(fiber: Fiber) { addFiber(fiber: Fiber) {
@@ -54,18 +37,18 @@ export class Scheduler {
} }
} }
} }
// if (this.shouldFlush === false) {
// this.shouldFlush = true; if (this.frame === 0) {
this.requestAnimationFrame(() => { this.frame = this.requestAnimationFrame(() => {
// this.shouldFlush = false; this.frame = 0;
this.tasks.forEach((fiber) => this.processFiber(fiber)); this.tasks.forEach((fiber) => this.processFiber(fiber));
for (let task of this.tasks) { for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) { if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task); this.tasks.delete(task);
}
} }
} });
}); }
// }
} }
processFiber(fiber: RootFiber) { processFiber(fiber: RootFiber) {
+3 -3
View File
@@ -129,9 +129,9 @@ class LazyValue {
/* /*
* Safely outputs `value` as a block depending on the nature of `value` * Safely outputs `value` as a block depending on the nature of `value`
*/ */
export function safeOutput(value: any): ReturnType<typeof toggler> { export function safeOutput(value: any, defaultValue?: any): ReturnType<typeof toggler> {
if (!value) { if (value === undefined) {
return value; return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
} }
let safeKey; let safeKey;
let block; let block;
+26
View File
@@ -1,5 +1,31 @@
export type Callback = () => void; export type Callback = () => void;
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve();
if (!called) {
called = true;
// 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.
// 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();
}
};
}
export function validateTarget(target: HTMLElement) { export function validateTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element"); throw new Error("Cannot mount component: the target is not a valid DOM element");
@@ -166,13 +166,13 @@ exports[`misc global 4`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput, withDefault } = helpers; let { safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const b3 = text(\`toto default\`); const b3 = text(\`toto default\`);
const b2 = withDefault(safeOutput(ctx['toto']), b3); const b2 = safeOutput(ctx['toto'], b3);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -133,6 +133,31 @@ exports[`t-esc t-esc is escaped 1`] = `
}" }"
`; `;
exports[`t-esc t-esc with the 0 number 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['var']);
}
}"
`;
exports[`t-esc t-esc with the 0 number, in a p 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['var'];
return block1([txt1]);
}
}"
`;
exports[`t-esc t-esc work with spread operator 1`] = ` exports[`t-esc t-esc work with spread operator 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -183,6 +208,17 @@ exports[`t-esc t-esc=0 is escaped 2`] = `
}" }"
`; `;
exports[`t-esc top level t-esc with undefined 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['var']);
}
}"
`;
exports[`t-esc variable 1`] = ` exports[`t-esc variable 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -217,13 +217,13 @@ exports[`t-out t-out on a node with a body, as a default 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput, withDefault } = helpers; let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`); let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const b3 = text(\`nope\`); const b3 = text(\`nope\`);
const b2 = withDefault(safeOutput(ctx['var']), b3); const b2 = safeOutput(ctx['var'], b3);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -233,14 +233,14 @@ exports[`t-out t-out on a node with a dom node in body, as a default 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput, withDefault } = helpers; let { safeOutput } = helpers;
let block1 = createBlock(\`<span><block-child-0/></span>\`); let block1 = createBlock(\`<span><block-child-0/></span>\`);
let block3 = createBlock(\`<div>nope</div>\`); let block3 = createBlock(\`<div>nope</div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const b3 = block3(); const b3 = block3();
const b2 = withDefault(safeOutput(ctx['var']), b3); const b2 = safeOutput(ctx['var'], b3);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -407,6 +407,45 @@ exports[`t-out t-out with just a t-set t-value in body 1`] = `
}" }"
`; `;
exports[`t-out t-out with the 0 number 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['var']);
}
}"
`;
exports[`t-out t-out with the 0 number, in a p 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = safeOutput(ctx['var']);
return block1([], [b2]);
}
}"
`;
exports[`t-out top level t-out with undefined 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['var']);
}
}"
`;
exports[`t-out variable 1`] = ` exports[`t-out variable 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -534,13 +534,36 @@ exports[`t-set t-set with t-value (truthy) and body 1`] = `
}" }"
`; `;
exports[`t-set t-set, multiple t-ifs, and a specific configuration 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<p><div><span>First div</span></div><div><block-child-0/></div></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
let b2;
if (ctx['flag']) {
setContextValue(ctx, \\"bouh\\", 2);
}
if (!ctx['flag']) {
b2 = text(\`Second\`);
}
return block1([], [b2]);
}
}"
`;
exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = ` exports[`t-set t-set, t-if, and mix of expression/body lookup, 1 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers; let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/><block-text-0/></div>\`); let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx); ctx = Object.create(ctx);
@@ -562,7 +585,7 @@ exports[`t-set t-set, t-if, and mix of expression/body lookup, 2 1`] = `
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers; let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/><block-text-0/></div>\`); let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx); ctx = Object.create(ctx);
+15
View File
@@ -67,6 +67,21 @@ describe("t-esc", () => {
); );
}); });
test("t-esc with the 0 number", () => {
const template = `<t t-esc="var"/>`;
expect(renderToString(template, { var: 0 })).toBe("0");
});
test("t-esc with the 0 number, in a p", () => {
const template = `<p><t t-esc="var"/></p>`;
expect(renderToString(template, { var: 0 })).toBe("<p>0</p>");
});
test("top level t-esc with undefined", () => {
const template = `<t t-esc="var"/>`;
expect(renderToString(template, { var: undefined })).toBe("");
});
test("falsy values in text nodes", () => { test("falsy values in text nodes", () => {
const template = ` const template = `
<t t-esc="v1"/>:<t t-esc="v2"/>:<t t-esc="v3"/>:<t t-esc="v4"/>:<t t-esc="v5"/>`; <t t-esc="v1"/>:<t t-esc="v2"/>:<t t-esc="v3"/>:<t t-esc="v4"/>:<t t-esc="v5"/>`;
+15
View File
@@ -47,6 +47,21 @@ describe("t-out", () => {
expect(renderToString(template, { var: new String("ok") })).toBe("<span>ok</span>"); expect(renderToString(template, { var: new String("ok") })).toBe("<span>ok</span>");
}); });
test("t-out with the 0 number", () => {
const template = `<t t-out="var"/>`;
expect(renderToString(template, { var: 0 })).toBe("0");
});
test("t-out with the 0 number, in a p", () => {
const template = `<p><t t-out="var"/></p>`;
expect(renderToString(template, { var: 0 })).toBe("<p>0</p>");
});
test("top level t-out with undefined", () => {
const template = `<t t-out="var"/>`;
expect(renderToString(template, { var: undefined })).toBe("");
});
test("with an extended String class", () => { test("with an extended String class", () => {
class LoveString extends String { class LoveString extends String {
valueOf(): string { valueOf(): string {
+16
View File
@@ -33,6 +33,22 @@ describe("t-set", () => {
expect(renderToString(template, { value: "ok" })).toBe("<div>grimbergen</div>"); expect(renderToString(template, { value: "ok" })).toBe("<div>grimbergen</div>");
}); });
test("t-set, multiple t-ifs, and a specific configuration", () => {
const template = `
<p>
<div>
<t t-if="flag" t-set="bouh" t-value="2"/>
<span>First div</span>
</div>
<div>
<t t-if="!flag">Second</t>
</div>
</p>`;
expect(renderToString(template)).toBe(
"<p><div><span>First div</span></div><div>Second</div></p>"
);
});
test("set from body literal", () => { test("set from body literal", () => {
const template = `<t><t t-set="value">ok</t><t t-esc="value"/></t>`; const template = `<t><t t-set="value">ok</t><t t-esc="value"/></t>`;
expect(renderToString(template)).toBe("ok"); expect(renderToString(template)).toBe("ok");
@@ -1308,6 +1308,18 @@ exports[`t-out in components can switch the contents of two t-out repeatedly 1`]
}" }"
`; `;
exports[`t-out in components t-out and updating falsy values, 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['state'].a);
}
}"
`;
exports[`t-out in components update properly on state changes 1`] = ` exports[`t-out in components update properly on state changes 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -1730,10 +1730,10 @@ exports[`renderings, destruction, patch, stuff, ... yet another variation 3`] =
) { ) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`); let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`); const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx]; let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val; let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]); const b3 = block3([hdlr1, txt1]);
@@ -1747,10 +1747,10 @@ exports[`renderings, destruction, patch, stuff, ... yet another variation 4`] =
) { ) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`); let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`); const b2 = text(\`C\`);
let hdlr1 = [ctx['increment'], ctx]; let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val; let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]); const b3 = block3([hdlr1, txt1]);
@@ -131,19 +131,6 @@ exports[`can catch errors an error in onWillDestroy 2`] = `
}" }"
`; `;
exports[`can catch errors an error in onWillDestroy 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, 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`] = ` exports[`can catch errors an error in onWillDestroy, variation 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -242,7 +242,7 @@ exports[`t-set t-set in t-if 1`] = `
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers; let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/><block-child-0/><p><block-text-0/></p></div>\`); let block1 = createBlock(\`<div><p><block-text-0/></p></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx); ctx = Object.create(ctx);
@@ -307,7 +307,7 @@ exports[`t-set t-set outside modified in t-if 1`] = `
let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers; let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-0/><block-child-0/><p><block-text-0/></p></div>\`); let block1 = createBlock(\`<div><p><block-text-0/></p></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx); ctx = Object.create(ctx);
+21
View File
@@ -1084,4 +1084,25 @@ describe("t-out in components", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div><div>2</div>"); expect(fixture.innerHTML).toBe("<div>1</div><div>2</div>");
}); });
test("t-out and updating falsy values, ", async () => {
class Test extends Component {
static template = xml`<t t-out="state.a"/>`;
state: any = useState({ a: 0 });
}
const comp = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("0");
comp.state.a = undefined;
await nextTick();
expect(fixture.innerHTML).toBe("");
comp.state.a = "hello";
await nextTick();
expect(fixture.innerHTML).toBe("hello");
comp.state.a = false;
await nextTick();
expect(fixture.innerHTML).toBe("false");
});
}); });
+39 -46
View File
@@ -103,13 +103,15 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect(n).toBe(0); expect(n).toBe(0);
w.state.val = 2; w.state.val = 2;
await nextTick(); await nextMicroTick();
await nextMicroTick();
expect(n).toBe(1); expect(n).toBe(1);
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged(); expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged();
w.state.val = 3; w.state.val = 3;
await nextTick(); await nextMicroTick();
await nextMicroTick();
expect(n).toBe(2); expect(n).toBe(2);
expect([ expect([
@@ -174,16 +176,14 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered", "Parent:rendered",
"Child:willDestroy", "Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:mounted", "Child:mounted",
"Parent:patched", "Parent:patched",
@@ -319,9 +319,9 @@ test("creating two async components, scenario 2", async () => {
"Parent:willRender", "Parent:willRender",
"ChildA:setup", "ChildA:setup",
"ChildA:willStart", "ChildA:willStart",
"Parent:rendered",
"ChildA:willRender", "ChildA:willRender",
"ChildA:rendered", "ChildA:rendered",
"Parent:rendered",
"ChildA:mounted", "ChildA:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -2235,13 +2235,13 @@ test("concurrent renderings scenario 16", async () => {
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"A:rendered",
"B:willRender", "B:willRender",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"B:rendered",
"C:willRender", "C:willRender",
"C:rendered", "C:rendered",
"B:rendered",
"A:rendered",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
"A:mounted", "A:mounted",
@@ -2906,9 +2906,9 @@ test("two sequential renderings before an animation frame", async () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -2920,7 +2920,13 @@ test("two sequential renderings before an animation frame", async () => {
await nextMicroTick(); await nextMicroTick();
await nextMicroTick(); await nextMicroTick();
expect(fixture.innerHTML).toBe("0"); expect(fixture.innerHTML).toBe("0");
expect([]).toBeLogged(); expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
parent.state.value = 2; parent.state.value = 2;
// enough microticks to wait for render + willupdateprops // enough microticks to wait for render + willupdateprops
@@ -2930,21 +2936,17 @@ test("two sequential renderings before an animation frame", async () => {
await nextMicroTick(); await nextMicroTick();
await nextMicroTick(); await nextMicroTick();
expect(fixture.innerHTML).toBe("0"); expect(fixture.innerHTML).toBe("0");
expect([]).toBeLogged();
await nextTick();
// we check here that the willPatch and patched hooks are called only once
expect([ expect([
"Parent:willRender", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged(); ]).toBeLogged();
await nextTick();
// we check here that the willPatch and patched hooks are called only once
expect(["Parent:willPatch", "Child:willPatch", "Child:patched", "Parent:patched"]).toBeLogged();
}); });
test("t-key on dom node having a component", async () => { test("t-key on dom node having a component", async () => {
@@ -3734,15 +3736,15 @@ test("delayed fiber does not get rendered if it was cancelled", async () => {
"A:setup", "A:setup",
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"A:rendered",
"B:willRender", "B:willRender",
"C:setup", "C:setup",
"B:rendered",
"C:willRender", "C:willRender",
"D:setup", "D:setup",
"C:rendered",
"D:willRender", "D:willRender",
"D:rendered", "D:rendered",
"C:rendered",
"B:rendered",
"A:rendered",
"D:mounted", "D:mounted",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
@@ -3751,7 +3753,7 @@ test("delayed fiber does not get rendered if it was cancelled", async () => {
// Start a render in C // Start a render in C
c!.render(true); c!.render(true);
await nextMicroTick(); await nextMicroTick();
expect(["C:willRender", "D:willRender", "D:rendered", "C:rendered"]).toBeLogged(); expect(["C:willRender", "C:rendered"]).toBeLogged();
// Start a render in A such that C is already rendered, but D will be delayed // 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) // (because A is rendering) then cancelled (when the render from A reaches C)
a.render(true); a.render(true);
@@ -3759,13 +3761,13 @@ test("delayed fiber does not get rendered if it was cancelled", async () => {
await nextTick(); await nextTick();
expect([ expect([
"A:willRender", "A:willRender",
"A:rendered",
"B:willRender", "B:willRender",
"B:rendered",
"C:willRender", "C:willRender",
"C:rendered",
"D:willRender", "D:willRender",
"D:rendered", "D:rendered",
"C:rendered",
"B:rendered",
"A:rendered",
"A:willPatch", "A:willPatch",
"B:willPatch", "B:willPatch",
"C:willPatch", "C:willPatch",
@@ -3777,7 +3779,7 @@ test("delayed fiber does not get rendered if it was cancelled", async () => {
]).toBeLogged(); ]).toBeLogged();
}); });
test.only("destroyed component causes other soon to be destroyed component to rerender, weird stuff happens", async () => { test("destroyed component causes other soon to be destroyed component to rerender, weird stuff happens", async () => {
let def = makeDeferred(); let def = makeDeferred();
let c: any = null; let c: any = null;
@@ -3785,9 +3787,6 @@ test.only("destroyed component causes other soon to be destroyed component to re
static template = xml`<t t-esc="props.value"/>`; static template = xml`<t t-esc="props.value"/>`;
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
onWillStart(async () => {
await nextMicroTick();
});
onRendered(() => { onRendered(() => {
def.resolve(); def.resolve();
}); });
@@ -3803,9 +3802,6 @@ test.only("destroyed component causes other soon to be destroyed component to re
setup() { setup() {
c = this; c = this;
useLogLifecycle(); useLogLifecycle();
onWillStart(async () => {
await nextMicroTick();
})
} }
} }
@@ -3820,9 +3816,6 @@ test.only("destroyed component causes other soon to be destroyed component to re
state = useState({ flag: false, valueB: 1, valueC: 2 }); state = useState({ flag: false, valueB: 1, valueC: 2 });
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
onWillStart(async () => {
await nextMicroTick();
})
} }
} }
@@ -3924,17 +3917,17 @@ test("delayed rendering, destruction, stuff happens", async () => {
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"A:rendered",
"B:willRender", "B:willRender",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"B:rendered",
"C:willRender", "C:willRender",
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"C:rendered",
"D:willRender", "D:willRender",
"D:rendered", "D:rendered",
"C:rendered",
"B:rendered",
"A:rendered",
"D:mounted", "D:mounted",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
@@ -3947,10 +3940,10 @@ test("delayed rendering, destruction, stuff happens", async () => {
expect([ expect([
"A:willRender", "A:willRender",
"B:willUpdateProps", "B:willUpdateProps",
"A:rendered",
"B:willRender", "B:willRender",
"C:willUpdateProps", "C:willUpdateProps",
"B:rendered", "B:rendered",
"A:rendered",
]).toBeLogged(); ]).toBeLogged();
// update B => removes child C // update B => removes child C
@@ -4025,19 +4018,19 @@ test("renderings, destruction, patch, stuff, ... yet another variation", async (
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"D:setup",
"D:willStart",
"A:rendered",
"B:willRender", "B:willRender",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"C:willRender",
"C:rendered",
"B:rendered", "B:rendered",
"D:setup",
"D:willStart",
"D:willRender", "D:willRender",
"D:rendered", "D:rendered",
"A:rendered", "C:willRender",
"D:mounted", "C:rendered",
"C:mounted", "C:mounted",
"D:mounted",
"B:mounted", "B:mounted",
"A:mounted", "A:mounted",
]).toBeLogged(); ]).toBeLogged();
+13 -12
View File
@@ -3,10 +3,10 @@ import {
onError, onError,
onMounted, onMounted,
onPatched, onPatched,
onRendered,
onWillPatch, onWillPatch,
onWillRender,
onWillStart, onWillStart,
onWillRender,
onRendered,
onWillUnmount, onWillUnmount,
useState, useState,
xml, xml,
@@ -14,8 +14,8 @@ import {
import { import {
logStep, logStep,
makeTestFixture, makeTestFixture,
nextMicroTick,
nextTick, nextTick,
nextMicroTick,
snapshotEverything, snapshotEverything,
useLogLifecycle, useLogLifecycle,
} from "../helpers"; } from "../helpers";
@@ -1251,9 +1251,9 @@ describe("can catch errors", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -1276,7 +1276,7 @@ describe("can catch errors", () => {
expect(fixture.innerHTML).toBe("2"); expect(fixture.innerHTML).toBe("2");
}); });
test.only("an error in onWillDestroy, variation", async () => { test("an error in onWillDestroy, variation", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<div>abc</div>`; static template = xml`<div>abc</div>`;
setup() { setup() {
@@ -1314,26 +1314,27 @@ describe("can catch errors", () => {
]).toBeLogged(); ]).toBeLogged();
parent.state.hasChild = true; parent.state.hasChild = true;
await nextTick(); await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([ expect([
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
]).toBeLogged(); ]).toBeLogged();
parent.state.hasChild = false; parent.state.hasChild = false;
await nextTick(); await nextTick();
expect([ expect([
"Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Parent:rendered", "Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willUnmount", "Parent:patched",
"Child:willDestroy",
]).toBeLogged(); ]).toBeLogged();
expect(fixture.innerHTML).toBe("2"); expect(fixture.innerHTML).toBe("2");
}); });
+27 -37
View File
@@ -461,9 +461,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -474,9 +474,9 @@ describe("lifecycle hooks", () => {
expect([ expect([
"Parent:willRender", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
@@ -520,9 +520,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -638,9 +638,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -694,13 +694,13 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"GrandChild:setup", "GrandChild:setup",
"GrandChild:willStart", "GrandChild:willStart",
"Child:rendered",
"GrandChild:willRender", "GrandChild:willRender",
"GrandChild:rendered", "GrandChild:rendered",
"Child:rendered",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"GrandChild:mounted", "GrandChild:mounted",
"Child:mounted", "Child:mounted",
@@ -803,11 +803,11 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"GrandChild:setup", "GrandChild:setup",
"GrandChild:willStart", "GrandChild:willStart",
"Child:rendered", "Child:rendered",
"Parent:rendered",
]).toBeLogged(); ]).toBeLogged();
app.destroy(); app.destroy();
@@ -843,9 +843,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -886,9 +886,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -898,9 +898,9 @@ describe("lifecycle hooks", () => {
expect([ expect([
"Parent:willRender", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
@@ -943,9 +943,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -1060,21 +1060,21 @@ describe("lifecycle hooks", () => {
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"B:willRender",
"B:rendered",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender", "C:willRender",
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"D:willRender",
"D:rendered",
"E:setup", "E:setup",
"E:willStart", "E:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"E:willRender", "E:willRender",
"E:rendered", "E:rendered",
"C:rendered",
"A:rendered",
"E:mounted", "E:mounted",
"D:mounted", "D:mounted",
"C:mounted", "C:mounted",
@@ -1089,9 +1089,9 @@ describe("lifecycle hooks", () => {
"C:willRender", "C:willRender",
"F:setup", "F:setup",
"F:willStart", "F:willStart",
"C:rendered",
"F:willRender", "F:willRender",
"F:rendered", "F:rendered",
"C:rendered",
"C:willPatch", "C:willPatch",
"E:willUnmount", "E:willUnmount",
"E:willDestroy", "E:willDestroy",
@@ -1124,9 +1124,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -1148,9 +1148,9 @@ describe("lifecycle hooks", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:mounted", "Child:mounted",
"Parent:patched", "Parent:patched",
@@ -1171,7 +1171,7 @@ describe("lifecycle hooks", () => {
} }
await mount(Parent, fixture); await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>Patched</span>"); expect(fixture.innerHTML).toBe("<span></span>");
expect([ expect([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
@@ -1180,13 +1180,11 @@ describe("lifecycle hooks", () => {
"Parent:mounted", "Parent:mounted",
"Parent:willRender", "Parent:willRender",
"Parent:rendered", "Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged(); ]).toBeLogged();
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<span>Patched</span>"); expect(fixture.innerHTML).toBe("<span>Patched</span>");
expect([]).toBeLogged(); expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
}); });
test("render in patched", async () => { test("render in patched", async () => {
@@ -1217,7 +1215,7 @@ describe("lifecycle hooks", () => {
parent.render(); parent.render();
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<span>Patched</span>"); expect(fixture.innerHTML).toBe("<span></span>");
expect([ expect([
"Parent:willRender", "Parent:willRender",
"Parent:rendered", "Parent:rendered",
@@ -1225,13 +1223,11 @@ describe("lifecycle hooks", () => {
"Parent:patched", "Parent:patched",
"Parent:willRender", "Parent:willRender",
"Parent:rendered", "Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged(); ]).toBeLogged();
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<span>Patched</span>"); expect(fixture.innerHTML).toBe("<span>Patched</span>");
expect([]).toBeLogged(); expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
}); });
test("render in willPatch", async () => { test("render in willPatch", async () => {
@@ -1269,15 +1265,12 @@ describe("lifecycle hooks", () => {
"Parent:rendered", "Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Parent:patched", "Parent:patched",
"Parent:willRender",
"Parent:rendered",
]).toBeLogged(); ]).toBeLogged();
await nextTick(); await nextTick();
expect([ expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("<span>Patched</span>"); expect(fixture.innerHTML).toBe("<span>Patched</span>");
}); });
@@ -1340,9 +1333,6 @@ describe("lifecycle hooks", () => {
static template = xml`child`; static template = xml`child`;
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
onWillStart(async () => {
await nextMicroTick();
});
} }
} }
+2 -2
View File
@@ -152,7 +152,7 @@ describe("reactivity in lifecycle", () => {
const prom = mount(Comp, fixture); const prom = mount(Comp, fixture);
(STATE as any).val = 2; (STATE as any).val = 2;
await prom; await prom;
expect(steps).toEqual([1, 2]); expect(steps).toEqual([2]);
expect(fixture.innerHTML).toBe("<div>2</div>"); expect(fixture.innerHTML).toBe("<div>2</div>");
}); });
@@ -180,9 +180,9 @@ describe("reactivity in lifecycle", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
+27 -37
View File
@@ -1,10 +1,11 @@
import { Component, mount, onRendered, onWillUpdateProps, useState, xml } from "../../src"; import { Component, mount, onRendered, onWillUpdateProps, useState, xml } from "../../src";
import { import {
makeDeferred,
makeTestFixture, makeTestFixture,
nextTick,
snapshotEverything, snapshotEverything,
nextTick,
useLogLifecycle, useLogLifecycle,
makeDeferred,
nextMicroTick,
} from "../helpers"; } from "../helpers";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -46,9 +47,9 @@ describe("rendering semantics", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -138,16 +139,11 @@ describe("rendering semantics", () => {
expect(childN).toBe(1); expect(childN).toBe(1);
}); });
test.only("render with deep=true followed by render with deep=false work as expected", async () => { test("render with deep=true followed by render with deep=false work as expected", async () => {
class Child extends Component { class Child extends Component {
static template = xml`child<t t-esc="env.getValue()"/>`; static template = xml`child<t t-esc="env.getValue()"/>`;
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
onRendered(() => {
if (value === 4) {
parent.state.value = "B";
}
});
} }
} }
@@ -177,9 +173,9 @@ describe("rendering semantics", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -188,39 +184,33 @@ describe("rendering semantics", () => {
parent.render(true); parent.render(true);
// wait for child to be rendered, but dom not yet patched // wait for child to be rendered, but dom not yet patched
// await nextMicroTick(); await nextMicroTick();
// await nextMicroTick(); await nextMicroTick();
// await nextMicroTick(); await nextMicroTick();
// expect([
// "Parent:willRender",
// "Child:willUpdateProps",
// "Child:willRender",
// "Child:rendered",
// "Parent:rendered",
// ]).toBeLogged();
// parent.state.value = "B";
await nextTick();
// await nextTick();
expect([ expect([
"Parent:willRender", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered", ]).toBeLogged();
parent.state.value = "B";
await nextTick();
expect(fixture.innerHTML).toBe("parentBchild4");
expect([
"Parent:willRender", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
"Parent:patched", "Parent:patched",
]).toBeLogged(); ]).toBeLogged();
expect(fixture.innerHTML).toBe("parentBchild4");
}); });
test("props are reactive", async () => { test("props are reactive", async () => {
@@ -251,9 +241,9 @@ describe("rendering semantics", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -294,9 +284,9 @@ describe("rendering semantics", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -382,13 +372,13 @@ describe("rendering semantics", () => {
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"A:rendered",
"B:willRender", "B:willRender",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"B:rendered",
"C:willRender", "C:willRender",
"C:rendered", "C:rendered",
"B:rendered",
"A:rendered",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
"A:mounted", "A:mounted",
@@ -447,13 +437,13 @@ test("force render in case of existing render", async () => {
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"A:rendered",
"B:willRender", "B:willRender",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"B:rendered",
"C:willRender", "C:willRender",
"C:rendered", "C:rendered",
"B:rendered",
"A:rendered",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
"A:mounted", "A:mounted",
@@ -476,9 +466,9 @@ test("force render in case of existing render", async () => {
expect([ expect([
"B:willRender", "B:willRender",
"C:willUpdateProps", "C:willUpdateProps",
"B:rendered",
"C:willRender", "C:willRender",
"C:rendered", "C:rendered",
"B:rendered",
"A:willPatch", "A:willPatch",
"B:willPatch", "B:willPatch",
"C:willPatch", "C:willPatch",
@@ -519,9 +509,9 @@ test("children, default props and renderings", async () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
+85 -23
View File
@@ -10,6 +10,7 @@ import {
toRaw, toRaw,
} from "../src"; } from "../src";
import { reactive, Reactive } from "../src/runtime/reactivity"; import { reactive, Reactive } from "../src/runtime/reactivity";
import { batched } from "../src/runtime/utils";
import { import {
makeDeferred, makeDeferred,
makeTestFixture, makeTestFixture,
@@ -189,6 +190,64 @@ describe("Reactivity", () => {
expect(n).toBe(2); expect(n).toBe(2);
}); });
test("batched: callback is called after batch of operation", async () => {
let n = 0;
const state = createReactive(
{ a: 1, b: 2 },
batched(() => n++)
);
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);
state.b = state.b + 5; // key is read and then modified
expect(n).toBe(0);
await nextMicroTick();
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 () => { test("setting property to same value does not trigger callback", async () => {
let n = 0; let n = 0;
const state = createReactive({ a: 1 }, () => n++); const state = createReactive({ a: 1 }, () => n++);
@@ -1713,13 +1772,13 @@ describe("Reactivity: useState", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
@@ -1767,13 +1826,13 @@ describe("Reactivity: useState", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
@@ -1781,17 +1840,18 @@ describe("Reactivity: useState", () => {
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>"); expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.value = 321; testContext.value = 321;
await nextTick(); await nextMicroTick();
await nextMicroTick();
expect([ expect([
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willPatch",
"Child:patched",
"Child:willPatch",
"Child:patched"
]).toBeLogged(); ]).toBeLogged();
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
await nextTick();
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>"); expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
}); });
@@ -1831,17 +1891,17 @@ describe("Reactivity: useState", () => {
"GrandFather:willRender", "GrandFather:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"GrandFather:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"GrandFather:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
"Child:mounted", "Child:mounted",
@@ -1849,18 +1909,19 @@ describe("Reactivity: useState", () => {
]).toBeLogged(); ]).toBeLogged();
testContext.value = 321; testContext.value = 321;
await nextTick(); await nextMicroTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><div><span>321</span></div></div>"); await nextMicroTick();
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
expect([ expect([
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willPatch",
"Child:patched",
"Child:willPatch",
"Child:patched"
]).toBeLogged(); ]).toBeLogged();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><div><span>321</span></div></div>");
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
}); });
test("one components can subscribe twice to same context", async () => { test("one components can subscribe twice to same context", async () => {
@@ -2042,9 +2103,9 @@ describe("Reactivity: useState", () => {
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]).toBeLogged(); ]).toBeLogged();
@@ -2161,7 +2222,8 @@ describe("Reactivity: useState", () => {
steps.clear(); steps.clear();
delete testContext[2]; delete testContext[2];
await nextTick(); await nextMicroTick();
await nextMicroTick();
expect([...steps]).toEqual(["list"]); expect([...steps]).toEqual(["list"]);
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>"); expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
+39 -1
View File
@@ -1,4 +1,5 @@
import { EventBus } from "../src/runtime/utils"; import { batched, EventBus } from "../src/runtime/utils";
import { nextMicroTick } from "./helpers";
describe("event bus behaviour", () => { describe("event bus behaviour", () => {
test("can subscribe and be notified", () => { test("can subscribe and be notified", () => {
@@ -33,3 +34,40 @@ describe("event bus behaviour", () => {
bus.trigger("event", "hello world"); bus.trigger("event", "hello world");
}); });
}); });
describe("batched", () => {
test("callback is called only once after operations", async () => {
let n = 0;
let fn = batched(() => n++);
expect(n).toBe(0);
fn();
fn();
expect(n).toBe(0);
await nextMicroTick();
expect(n).toBe(1);
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);
});
});