Compare commits

..

14 Commits

Author SHA1 Message Date
Géry Debongnie 588b655c11 [REL] v2.0.0-beta-15
# v2.0.0-beta-15

Notes

- fix: lifecyle_hooks: correctly wrap errors in async code
- imp: use a custom error class for all errors thrown by owl
- fix: package.json: remove browser value
- ref: component_node: slightly simplify code
2022-07-20 10:02:24 +02:00
Géry Debongnie f5d5273c25 [REF] component_node: slightly simplify code 2022-07-20 09:57:27 +02:00
Géry Debongnie 9fd662fdce [FIX] package.json: remove browser value
As far as I can tell, the browser value overrides the main value in many
cases.  But then, we don't want to use the iife format, since it don't
work well with bundlers. This commit fixes the issue by simply removing
the key, so the main entry will be used instead.

maybe fixes #1181
2022-07-20 09:56:39 +02:00
Samuel Degueldre 7786077921 [IMP] *: use a custom error class for all errors thrown by owl
This commit makes all errors thrown in owl use a custom error class. The
main point of this is to always wrap user-code errors that happen during
the owl lifecycle so that they can be treated uniformly in onError by
checking the cause property, and also allows user code to differenciate
owl errors from non-owl errors reliably at runtime.
2022-07-18 15:40:14 +02:00
Aaron Bohy 30bc605c84 [FIX] lifecyle_hooks: correctly wrap errors in async code
Before this commit, wrapping an error occurring in async code
would result in an unhandledpromise exception, because we created
another promise, that would be rejected and that we didn't catch.
2022-07-18 15:40:14 +02:00
Géry Debongnie d1118455aa [REL] v2.0.0-beta-14
# v2.0.0-beta-14

Yes, there is no beta-13 release...

## Fixes:

- [FIX] compiler: better handle update of properties with same value
2022-07-08 16:11:45 +02:00
Géry Debongnie f8073cb153 [FIX] compiler: better handle update of properties with same value
Before this commit, owl would incorrectly skip patching html properties
when the new value is the same as the value used in the previous render.
However, this is incorrect, since the user may have changed the value by
clicking on a checkbox, or changing some text in an input.

So, to be more correct, owl has to force the update whenever it
encounters a prop.  This is however hard to do without impacting the
main update loop, so we kind of work around the problem by using String
and Boolean instance, instead of primitive values.
2022-07-08 15:58:49 +02:00
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
47 changed files with 941 additions and 423 deletions
+1 -2
View File
@@ -1,9 +1,8 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.0-beta-10", "version": "2.0.0-beta-15",
"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",
"module": "dist/owl.es.js", "module": "dist/owl.es.js",
"types": "dist/types/owl.d.ts", "types": "dist/types/owl.d.ts",
"files": [ "files": [
+16 -8
View File
@@ -6,6 +6,14 @@ import dts from "rollup-plugin-dts";
let input, output; let input, output;
const IIFE_FILENAME = "dist/owl.iife.js";
const CJS_FILENAME = "dist/owl.cjs.js";
const ES_FILENAME = "dist/owl.es.js";
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
throw new Error("package.json has been modified. Build script should be updated accordingly");
}
const outro = ` const outro = `
__info__.version = '${pkg.version}'; __info__.version = '${pkg.version}';
__info__.date = '${new Date().toISOString()}'; __info__.date = '${new Date().toISOString()}';
@@ -23,19 +31,19 @@ switch (process.argv[4]) {
case "runtime": case "runtime":
input = "src/runtime/index.ts"; input = "src/runtime/index.ts";
output = [ output = [
getConfigForFormat('esm', addSuffix(pkg.module, 'runtime'), outro), getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
getConfigForFormat('cjs', addSuffix(pkg.main, 'runtime'), outro), getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
getConfigForFormat('iife', addSuffix(pkg.browser, 'runtime'), outro), getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
getConfigForFormat('iife', addSuffix(pkg.browser, 'runtime'), outro, true), getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
] ]
break; break;
default: default:
input = "src/index.ts", input = "src/index.ts",
output = [ output = [
getConfigForFormat('esm', pkg.module, outro), getConfigForFormat('esm', ES_FILENAME, outro),
getConfigForFormat('cjs', pkg.main, outro), getConfigForFormat('cjs', CJS_FILENAME, outro),
getConfigForFormat('iife', pkg.browser, outro), getConfigForFormat('iife', IIFE_FILENAME, outro),
getConfigForFormat('iife', pkg.browser, outro, true), getConfigForFormat('iife', IIFE_FILENAME, outro, true),
] ]
} }
+53 -41
View File
@@ -1,3 +1,4 @@
import { isProp } from "../runtime/blockdom/attributes";
import { import {
compileExpr, compileExpr,
compileExprToArray, compileExprToArray,
@@ -22,13 +23,14 @@ import {
ASTTif, ASTTif,
ASTTKey, ASTTKey,
ASTTOut, ASTTOut,
ASTTSet,
ASTTranslation,
ASTType,
ASTTPortal, ASTTPortal,
EventHandlers, ASTTranslation,
ASTTSet,
ASTType,
Attrs, Attrs,
EventHandlers,
} from "./parser"; } from "./parser";
import { OwlError } from "../runtime/error_handling";
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment"; type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
@@ -143,7 +145,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 +336,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);
} }
@@ -534,7 +536,7 @@ export class CodeGenerator {
.slice(1) .slice(1)
.map((m) => { .map((m) => {
if (!MODS.has(m)) { if (!MODS.has(m)) {
throw new Error(`Unknown event modifier: '${m}'`); throw new OwlError(`Unknown event modifier: '${m}'`);
} }
return `"${m}"`; return `"${m}"`;
}); });
@@ -578,13 +580,18 @@ export class CodeGenerator {
attrName = key.slice(7); attrName = key.slice(7);
attrs["block-attribute-" + idx] = attrName; attrs["block-attribute-" + idx] = attrName;
} else if (key.startsWith("t-att")) { } else if (key.startsWith("t-att")) {
attrName = key === "t-att" ? null : key.slice(6);
expr = compileExpr(ast.attrs[key]); expr = compileExpr(ast.attrs[key]);
if (attrName && isProp(ast.tag, attrName)) {
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
const C = attrName === "value" ? "String" : "Boolean";
expr = `new ${C}(${expr})`;
}
const idx = block!.insertData(expr, "attr"); const idx = block!.insertData(expr, "attr");
if (key === "t-att") { if (key === "t-att") {
attrs[`block-attributes`] = String(idx); attrs[`block-attributes`] = String(idx);
} else { } else {
attrName = key.slice(6); attrs[`block-attribute-${idx}`] = attrName!;
attrs[`block-attribute-${idx}`] = attrName;
} }
} else if (this.translatableAttributes.includes(key)) { } else if (this.translatableAttributes.includes(key)) {
attrs[key] = this.translateFn(ast.attrs[key]); attrs[key] = this.translateFn(ast.attrs[key]);
@@ -692,7 +699,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 +761,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 +801,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) {
@@ -861,8 +872,9 @@ export class CodeGenerator {
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar); this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
if (this.dev) { if (this.dev) {
// Throw error on duplicate keys in dev mode // Throw error on duplicate keys in dev mode
this.helpers.add("OwlError");
this.addLine( this.addLine(
`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}` `if (keys${block.id}.has(key${this.target.loopLevel})) { throw new OwlError(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`
); );
this.addLine(`keys${block.id}.add(key${this.target.loopLevel});`); this.addLine(`keys${block.id}.add(key${this.target.loopLevel});`);
} }
@@ -885,7 +897,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 +944,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 +990,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");
@@ -1084,7 +1096,7 @@ export class CodeGenerator {
name = _name; name = _name;
value = `bind(ctx, ${value || undefined})`; value = `bind(ctx, ${value || undefined})`;
} else { } else {
throw new Error("Invalid prop suffix"); throw new OwlError("Invalid prop suffix");
} }
} }
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`; name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
+5 -3
View File
@@ -1,3 +1,5 @@
import { OwlError } from "../runtime/error_handling";
/** /**
* Owl QWeb Expression Parser * Owl QWeb Expression Parser
* *
@@ -106,14 +108,14 @@ let tokenizeString: Tokenizer = function (expr) {
i++; i++;
cur = expr[i]; cur = expr[i];
if (!cur) { if (!cur) {
throw new Error("Invalid expression"); throw new OwlError("Invalid expression");
} }
s += cur; s += cur;
} }
i++; i++;
} }
if (expr[i] !== start) { if (expr[i] !== start) {
throw new Error("Invalid expression"); throw new OwlError("Invalid expression");
} }
s += start; s += start;
if (start === "`") { if (start === "`") {
@@ -223,7 +225,7 @@ export function tokenize(expr: string): Token[] {
error = e; // Silence all errors and throw a generic error below error = e; // Silence all errors and throw a generic error below
} }
if (current.length || error) { if (current.length || error) {
throw new Error(`Tokenizer error: could not tokenize \`${expr}\``); throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
} }
return result; return result;
} }
+23 -17
View File
@@ -1,3 +1,5 @@
import { OwlError } from "../runtime/error_handling";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// AST Type definition // AST Type definition
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -319,7 +321,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
return null; return null;
} }
if (tagName.startsWith("block-")) { if (tagName.startsWith("block-")) {
throw new Error(`Invalid tag name: '${tagName}'`); throw new OwlError(`Invalid tag name: '${tagName}'`);
} }
ctx = Object.assign({}, ctx); ctx = Object.assign({}, ctx);
if (tagName === "pre") { if (tagName === "pre") {
@@ -340,13 +342,15 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const value = node.getAttribute(attr)!; const value = node.getAttribute(attr)!;
if (attr.startsWith("t-on")) { if (attr.startsWith("t-on")) {
if (attr === "t-on") { if (attr === "t-on") {
throw new Error("Missing event name with t-on directive"); throw new OwlError("Missing event name with t-on directive");
} }
on = on || {}; on = on || {};
on[attr.slice(5)] = value; on[attr.slice(5)] = value;
} else if (attr.startsWith("t-model")) { } else if (attr.startsWith("t-model")) {
if (!["input", "select", "textarea"].includes(tagName)) { if (!["input", "select", "textarea"].includes(tagName)) {
throw new Error("The t-model directive only works with <input>, <textarea> and <select>"); throw new OwlError(
"The t-model directive only works with <input>, <textarea> and <select>"
);
} }
let baseExpr, expr; let baseExpr, expr;
@@ -359,7 +363,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
baseExpr = value.slice(0, index); baseExpr = value.slice(0, index);
expr = value.slice(index + 1, -1); expr = value.slice(index + 1, -1);
} else { } else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`); throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
} }
const typeAttr = node.getAttribute("type"); const typeAttr = node.getAttribute("type");
@@ -390,10 +394,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
ctx.tModelInfo = model; ctx.tModelInfo = model;
} }
} else if (attr.startsWith("block-")) { } else if (attr.startsWith("block-")) {
throw new Error(`Invalid attribute: '${attr}'`); throw new OwlError(`Invalid attribute: '${attr}'`);
} else if (attr !== "t-name") { } else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) { if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new Error(`Unknown QWeb directive: '${attr}'`); throw new OwlError(`Unknown QWeb directive: '${attr}'`);
} }
const tModel = ctx.tModelInfo; const tModel = ctx.tModelInfo;
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) { if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
@@ -447,7 +451,7 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
}; };
} }
if (ast.type === ASTType.TComponent) { if (ast.type === ASTType.TComponent) {
throw new Error("t-esc is not supported on Component nodes"); throw new OwlError("t-esc is not supported on Component nodes");
} }
return tesc; return tesc;
} }
@@ -503,7 +507,7 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
node.removeAttribute("t-as"); node.removeAttribute("t-as");
const key = node.getAttribute("t-key"); const key = node.getAttribute("t-key");
if (!key) { if (!key) {
throw new Error( throw new OwlError(
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")` `"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
); );
} }
@@ -686,7 +690,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let isDynamic = node.hasAttribute("t-component"); let isDynamic = node.hasAttribute("t-component");
if (isDynamic && name !== "t") { if (isDynamic && name !== "t") {
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`); throw new OwlError(
`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`
);
} }
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) { if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
@@ -713,7 +719,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
on[name.slice(5)] = value; on[name.slice(5)] = value;
} else { } else {
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-")); const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
throw new Error(message || `unsupported directive on Component: ${name}`); throw new OwlError(message || `unsupported directive on Component: ${name}`);
} }
} else { } else {
props = props || {}; props = props || {};
@@ -729,7 +735,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
for (let slotNode of slotNodes) { for (let slotNode of slotNodes) {
if (slotNode.tagName !== "t") { if (slotNode.tagName !== "t") {
throw new Error( throw new OwlError(
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)` `Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
); );
} }
@@ -904,7 +910,7 @@ function normalizeTIf(el: Element) {
let nattr = (name: string) => +!!node.getAttribute(name); let nattr = (name: string) => +!!node.getAttribute(name);
if (prevElem && (pattr("t-if") || pattr("t-elif"))) { if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
if (pattr("t-foreach")) { if (pattr("t-foreach")) {
throw new Error( throw new OwlError(
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else" "t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
); );
} }
@@ -913,19 +919,19 @@ function normalizeTIf(el: Element) {
return a + b; return a + b;
}) > 1 }) > 1
) { ) {
throw new Error("Only one conditional branching directive is allowed per node"); throw new OwlError("Only one conditional branching directive is allowed per node");
} }
// All text (with only spaces) and comment nodes (nodeType 8) between // All text (with only spaces) and comment nodes (nodeType 8) between
// branch nodes are removed // branch nodes are removed
let textNode; let textNode;
while ((textNode = node.previousSibling) !== prevElem) { while ((textNode = node.previousSibling) !== prevElem) {
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) { if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
throw new Error("text is not allowed between branching directives"); throw new OwlError("text is not allowed between branching directives");
} }
textNode!.remove(); textNode!.remove();
} }
} else { } else {
throw new Error( throw new OwlError(
"t-elif and t-else directives must be preceded by a t-if or t-elif directive" "t-elif and t-else directives must be preceded by a t-if or t-elif directive"
); );
} }
@@ -946,7 +952,7 @@ function normalizeTEsc(el: Element) {
); );
for (const el of elements) { for (const el of elements) {
if (el.childNodes.length) { if (el.childNodes.length) {
throw new Error("Cannot have t-esc on a component that already has content"); throw new OwlError("Cannot have t-esc on a component that already has content");
} }
const value = el.getAttribute("t-esc"); const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc"); el.removeAttribute("t-esc");
@@ -1000,7 +1006,7 @@ function parseXML(xml: string): XMLDocument {
} }
} }
} }
throw new Error(msg); throw new OwlError(msg);
} }
return doc; return doc;
+3 -3
View File
@@ -1,6 +1,6 @@
import { Component, ComponentConstructor, Props } from "./component"; import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node"; import { ComponentNode } from "./component_node";
import { nodeErrorHandlers } from "./error_handling"; import { nodeErrorHandlers, OwlError } from "./error_handling";
import { Fiber, MountOptions } from "./fibers"; import { Fiber, MountOptions } from "./fibers";
import { Scheduler } from "./scheduler"; import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers"; import { validateProps } from "./template_helpers";
@@ -154,9 +154,9 @@ export class App<
if (isStatic) { if (isStatic) {
C = parent.constructor.components[name as any]; C = parent.constructor.components[name as any];
if (!C) { if (!C) {
throw new Error(`Cannot find the definition of component "${name}"`); throw new OwlError(`Cannot find the definition of component "${name}"`);
} else if (!(C.prototype instanceof Component)) { } else if (!(C.prototype instanceof Component)) {
throw new Error( throw new OwlError(
`"${name}" is not a Component. It must inherit from the Component class` `"${name}" is not a Component. It must inherit from the Component class`
); );
} }
+1 -1
View File
@@ -144,7 +144,7 @@ export function updateClass(this: HTMLElement, val: any, oldVal: any) {
export function makePropSetter(name: string): Setter<HTMLElement> { export function makePropSetter(name: string): Setter<HTMLElement> {
return function setProp(this: HTMLElement, value: any) { return function setProp(this: HTMLElement, value: any) {
// support 0, fallback to empty string for other falsy values // support 0, fallback to empty string for other falsy values
(this as any)[name] = value === 0 ? 0 : value || ""; (this as any)[name] = value === 0 ? 0 : value ? value.valueOf() : "";
}; };
} }
+2 -1
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../error_handling";
import { import {
attrsSetter, attrsSetter,
attrsUpdater, attrsUpdater,
@@ -245,7 +246,7 @@ function buildTree(
}; };
} }
} }
throw new Error("boom"); throw new OwlError("boom");
} }
function addRef(tree: IntermediateTree) { function addRef(tree: IntermediateTree) {
+41 -59
View File
@@ -1,7 +1,7 @@
import type { App, Env } from "./app"; import type { App, Env } from "./app";
import { BDom, VNode } from "./blockdom"; import { BDom, VNode } from "./blockdom";
import { Component, ComponentConstructor, Props } from "./component"; import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError, handleError } from "./error_handling"; import { fibersInError, handleError, OwlError } from "./error_handling";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers"; import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { import {
clearReactivesForCallback, clearReactivesForCallback,
@@ -12,13 +12,13 @@ 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;
export function getCurrent(): ComponentNode { export function getCurrent(): ComponentNode {
if (!currentNode) { if (!currentNode) {
throw new Error("No active component (a hook function should only be called in 'setup')"); throw new OwlError("No active component (a hook function should only be called in 'setup')");
} }
return currentNode; return currentNode;
} }
@@ -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));
} }
@@ -83,7 +83,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
renderFn: Function; renderFn: Function;
parent: ComponentNode | null; parent: ComponentNode | null;
level: number;
childEnv: Env; childEnv: Env;
children: { [key: string]: ComponentNode } = Object.create(null); children: { [key: string]: ComponentNode } = Object.create(null);
refs: any = {}; refs: any = {};
@@ -108,7 +107,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.parent = parent; this.parent = parent;
this.props = props; this.props = props;
this.parentKey = parentKey; this.parentKey = parentKey;
this.level = parent ? parent.level + 1 : 0;
const defaultProps = C.defaultProps; const defaultProps = C.defaultProps;
props = Object.assign({}, props); props = Object.assign({}, props);
if (defaultProps) { if (defaultProps) {
@@ -140,22 +138,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 +151,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 +173,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 +245,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 +260,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 +347,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) : [];
} }
} }
+10 -1
View File
@@ -1,6 +1,11 @@
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers"; import type { Fiber } from "./fibers";
// Custom error class that wraps error that happen in the owl lifecycle
export class OwlError extends Error {
cause?: any;
}
// Maps fibers to thrown errors // Maps fibers to thrown errors
export const fibersInError: WeakMap<Fiber, any> = new WeakMap(); export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap(); export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
@@ -37,7 +42,11 @@ function _handleError(node: ComponentNode | null, error: any): boolean {
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber }); type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
export function handleError(params: ErrorParams) { export function handleError(params: ErrorParams) {
const error = params.error; let { error } = params;
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
if (!(error instanceof OwlError)) {
error = Object.assign(new OwlError("An error occured in the owl lifecycle"), { cause: error });
}
const node = "node" in params ? params.node : params.fiber.node; const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!; const fiber = "fiber" in params ? params.fiber : node.fiber!;
+2 -1
View File
@@ -1,5 +1,6 @@
import { filterOutModifiersFromData } from "./blockdom/config"; import { filterOutModifiersFromData } from "./blockdom/config";
import { STATUS } from "./status"; import { STATUS } from "./status";
import { OwlError } from "./error_handling";
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => { export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data); const { data: _data, modifiers } = filterOutModifiersFromData(data);
@@ -33,7 +34,7 @@ export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarg
if (Object.hasOwnProperty.call(data, 0)) { if (Object.hasOwnProperty.call(data, 0)) {
const handler = data[0]; const handler = data[0];
if (typeof handler !== "function") { if (typeof handler !== "function") {
throw new Error(`Invalid handler (expected a function, received: '${handler}')`); throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
} }
let node = data[1] ? data[1].__owl__ : null; let node = data[1] ? data[1].__owl__ : null;
if (node ? node.status === STATUS.MOUNTED : true) { if (node ? node.status === STATUS.MOUNTED : true) {
+2 -5
View File
@@ -1,6 +1,6 @@
import { BDom, mount } from "./blockdom"; import { BDom, mount } from "./blockdom";
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
import { fibersInError, handleError } from "./error_handling"; import { fibersInError, handleError, OwlError } from "./error_handling";
import { STATUS } from "./status"; import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
@@ -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 = [];
@@ -44,7 +43,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
} }
function throwOnRender() { function throwOnRender() {
throw new Error("Attempted to render cancelled fiber"); throw new OwlError("Attempted to render cancelled fiber");
} }
/** /**
@@ -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
+1
View File
@@ -54,5 +54,6 @@ export {
onError, onError,
} from "./lifecycle_hooks"; } from "./lifecycle_hooks";
export { validate } from "./validation"; export { validate } from "./validation";
export { OwlError } from "./error_handling";
export const __info__ = {}; export const __info__ = {};
+13 -15
View File
@@ -1,21 +1,28 @@
import { getCurrent } from "./component_node"; import { getCurrent } from "./component_node";
import { nodeErrorHandlers } from "./error_handling"; import { nodeErrorHandlers, OwlError } from "./error_handling";
const TIMEOUT = Symbol("timeout"); const TIMEOUT = Symbol("timeout");
function wrapError(fn: (...args: any[]) => any, hookName: string) { function wrapError(fn: (...args: any[]) => any, hookName: string) {
const error = new Error(`The following error occurred in ${hookName}: `) as Error & { const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
cause: any; cause: any;
}; };
const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`); const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
const node = getCurrent(); const node = getCurrent();
return (...args: any[]) => { return (...args: any[]) => {
const onError = (cause: any) => {
if (cause instanceof Error) {
error.cause = cause;
error.message += `"${cause.message}"`;
}
throw error;
};
try { try {
const result = fn(...args); const result = fn(...args);
if (result instanceof Promise) { if (result instanceof Promise) {
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") { if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
const fiber = node.fiber; const fiber = node.fiber;
Promise.race([ Promise.race([
result, result.catch(() => {}),
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)), new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
]).then((res) => { ]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber) { if (res === TIMEOUT && node.fiber === fiber) {
@@ -23,20 +30,11 @@ function wrapError(fn: (...args: any[]) => any, hookName: string) {
} }
}); });
} }
return result.catch((cause) => { return result.catch(onError);
error.cause = cause;
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
}
throw error;
});
} }
return result; return result;
} catch (cause) { } catch (cause) {
if (cause instanceof Error) { onError(cause);
error.message += `"${cause.message}"`;
}
throw error;
} }
}; };
} }
+2 -1
View File
@@ -1,6 +1,7 @@
import { onWillUnmount } from "./lifecycle_hooks"; import { onWillUnmount } from "./lifecycle_hooks";
import { BDom, text, VNode } from "./blockdom"; import { BDom, text, VNode } from "./blockdom";
import { Component } from "./component"; import { Component } from "./component";
import { OwlError } from "./error_handling";
const VText: any = text("").constructor; const VText: any = text("").constructor;
@@ -24,7 +25,7 @@ class VPortal extends VText implements Partial<VNode<VPortal>> {
} }
this.target = el && el.querySelector(this.selector); this.target = el && el.querySelector(this.selector);
if (!this.target) { if (!this.target) {
throw new Error("invalid portal target"); throw new OwlError("invalid portal target");
} }
} }
this.realBDom!.mount(this.target!, null); this.realBDom!.mount(this.target!, null);
+2 -1
View File
@@ -1,4 +1,5 @@
import { Callback } from "./utils"; import { Callback } from "./utils";
import { OwlError } from "./error_handling";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object) // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
export const TARGET = Symbol("Target"); export const TARGET = Symbol("Target");
@@ -197,7 +198,7 @@ export function reactive<T extends Target>(
callback: Callback = () => {} callback: Callback = () => {}
): Reactive<T> | NonReactive<T> { ): Reactive<T> | NonReactive<T> {
if (!canBeMadeReactive(target)) { if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`); throw new OwlError(`Cannot make the given value reactive`);
} }
if (SKIP in target) { if (SKIP in target) {
return target as NonReactive<T>; return target as NonReactive<T>;
+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) {
+11 -7
View File
@@ -4,6 +4,7 @@ import { html } from "./blockdom/index";
import { isOptional, validateSchema } from "./validation"; import { isOptional, validateSchema } from "./validation";
import type { ComponentConstructor } from "./component"; import type { ComponentConstructor } from "./component";
import { markRaw } from "./reactivity"; import { markRaw } from "./reactivity";
import { OwlError } from "./error_handling";
const ObjectCreate = Object.create; const ObjectCreate = Object.create;
/** /**
@@ -70,7 +71,7 @@ function prepareList(collection: any): [any[], any[], number, any[]] {
values = Object.keys(collection); values = Object.keys(collection);
keys = Object.values(collection); keys = Object.values(collection);
} else { } else {
throw new Error("Invalid loop expression"); throw new OwlError("Invalid loop expression");
} }
const n = values.length; const n = values.length;
return [keys, values, n, new Array(n)]; return [keys, values, n, new Array(n)];
@@ -129,9 +130,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;
@@ -191,7 +192,7 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
if (el) { if (el) {
count++; count++;
if (count > 1) { if (count > 1) {
throw new Error("Cannot have 2 elements with same ref name at the same time"); throw new OwlError("Cannot have 2 elements with same ref name at the same time");
} }
} }
if (count === 0 || el) { if (count === 0 || el) {
@@ -233,7 +234,7 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
: name in schema && !("*" in schema) && !isOptional(schema[name]); : name in schema && !("*" in schema) && !isOptional(schema[name]);
for (let p in defaultProps) { for (let p in defaultProps) {
if (isMandatory(p)) { if (isMandatory(p)) {
throw new Error( throw new OwlError(
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})` `A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
); );
} }
@@ -242,7 +243,9 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
const errors = validateSchema(props, schema); const errors = validateSchema(props, schema);
if (errors.length) { if (errors.length) {
throw new Error(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")); throw new OwlError(
`Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")
);
} }
} }
@@ -264,4 +267,5 @@ export const helpers = {
bind, bind,
createCatcher, createCatcher,
markRaw, markRaw,
OwlError,
}; };
+5 -4
View File
@@ -3,6 +3,7 @@ import { comment, createBlock, html, list, multi, text, toggler } from "./blockd
import { getCurrent } from "./component_node"; import { getCurrent } from "./component_node";
import { Portal, portalTemplate } from "./portal"; import { Portal, portalTemplate } from "./portal";
import { helpers } from "./template_helpers"; import { helpers } from "./template_helpers";
import { OwlError } from "./error_handling";
const bdom = { text, createBlock, list, multi, html, toggler, comment }; const bdom = { text, createBlock, list, multi, html, toggler, comment };
@@ -31,7 +32,7 @@ function parseXML(xml: string): Document {
} }
} }
} }
throw new Error(msg); throw new OwlError(msg);
} }
return doc; return doc;
} }
@@ -76,7 +77,7 @@ export class TemplateSet {
if (currentAsString === newAsString) { if (currentAsString === newAsString) {
return; return;
} }
throw new Error(`Template ${name} already defined with different content`); throw new OwlError(`Template ${name} already defined with different content`);
} }
this.rawTemplates[name] = template; this.rawTemplates[name] = template;
} }
@@ -102,7 +103,7 @@ export class TemplateSet {
const componentName = getCurrent().component.constructor.name; const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`; extraInfo = ` (for component "${componentName}")`;
} catch {} } catch {}
throw new Error(`Missing template: "${name}"${extraInfo}`); throw new OwlError(`Missing template: "${name}"${extraInfo}`);
} }
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element); const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate); const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
@@ -119,7 +120,7 @@ export class TemplateSet {
} }
_compileTemplate(name: string, template: string | Element): ReturnType<typeof compile> { _compileTemplate(name: string, template: string | Element): ReturnType<typeof compile> {
throw new Error(`Unable to compile a template. Please use owl full build instead`); throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
} }
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any { callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
+30 -3
View File
@@ -1,11 +1,38 @@
import { OwlError } from "./error_handling";
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 OwlError("Cannot mount component: the target is not a valid DOM element");
} }
if (!document.body.contains(target)) { if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node"); throw new OwlError("Cannot mount a component on a detached dom node");
} }
} }
@@ -28,7 +55,7 @@ export function whenReady(fn?: any): Promise<void> {
export async function loadFile(url: string): Promise<string> { export async function loadFile(url: string): Promise<string> {
const result = await fetch(url); const result = await fetch(url);
if (!result.ok) { if (!result.ok) {
throw new Error("Error while fetching xml templates"); throw new OwlError("Error while fetching xml templates");
} }
return await result.text(); return await result.text();
} }
+3 -1
View File
@@ -1,3 +1,5 @@
import { OwlError } from "./error_handling";
type BaseType = type BaseType =
| typeof String | typeof String
| typeof Boolean | typeof Boolean
@@ -70,7 +72,7 @@ function toSchema(spec: SimplifiedSchema): NormalizedSchema {
export function validate(obj: { [key: string]: any }, spec: Schema) { export function validate(obj: { [key: string]: any }, spec: Schema) {
let errors = validateSchema(obj, spec); let errors = validateSchema(obj, spec);
if (errors.length) { if (errors.length) {
throw new Error("Invalid object: " + errors.join(", ")); throw new OwlError("Invalid object: " + errors.join(", "));
} }
} }
@@ -745,7 +745,7 @@ exports[`special cases for some specific html attributes/properties input of typ
let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"indeterminate\\"/>\`); let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"indeterminate\\"/>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let attr1 = ctx['v']; let attr1 = new Boolean(ctx['v']);
return block1([attr1]); return block1([attr1]);
} }
}" }"
@@ -759,7 +759,21 @@ exports[`special cases for some specific html attributes/properties input type=
let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"checked\\"/>\`); let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"checked\\"/>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let attr1 = ctx['flag']; let attr1 = new Boolean(ctx['flag']);
return block1([attr1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties input with t-att-value (patching with same value 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
return block1([attr1]); return block1([attr1]);
} }
}" }"
@@ -773,7 +787,21 @@ exports[`special cases for some specific html attributes/properties input with t
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`); let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let attr1 = ctx['v']; let attr1 = new String(ctx['v']);
return block1([attr1]);
}
}"
`;
exports[`special cases for some specific html attributes/properties input, type checkbox, with t-att-checked (patching with same value 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<input type=\\"checkbox\\" block-attribute-0=\\"checked\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new Boolean(ctx['v']);
return block1([attr1]); return block1([attr1]);
} }
}" }"
@@ -787,7 +815,7 @@ exports[`special cases for some specific html attributes/properties select with
let block1 = createBlock(\`<select block-attribute-0=\\"value\\"><option value=\\"potato\\">Potato</option><option value=\\"tomato\\">Tomato</option><option value=\\"onion\\">Onion</option></select>\`); let block1 = createBlock(\`<select block-attribute-0=\\"value\\"><option value=\\"potato\\">Potato</option><option value=\\"tomato\\">Tomato</option><option value=\\"onion\\">Onion</option></select>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let attr1 = ctx['value']; let attr1 = new String(ctx['value']);
return block1([attr1]); return block1([attr1]);
} }
}" }"
@@ -801,7 +829,7 @@ exports[`special cases for some specific html attributes/properties textarea wit
let block1 = createBlock(\`<textarea block-attribute-0=\\"value\\"/>\`); let block1 = createBlock(\`<textarea block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let attr1 = ctx['v']; let attr1 = new String(ctx['v']);
return block1([attr1]); return block1([attr1]);
} }
}" }"
@@ -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]);
} }
}" }"
@@ -269,7 +269,7 @@ exports[`misc other complex template 1`] = `
ctx[\`category\`] = v_block15[i1]; ctx[\`category\`] = v_block15[i1];
const key1 = ctx['category'].id; const key1 = ctx['category'].id;
let attr6 = ctx['category'].id; let attr6 = ctx['category'].id;
let attr7 = ctx['category'].id==ctx['options'].active_category_id; let attr7 = new Boolean(ctx['category'].id==ctx['options'].active_category_id);
let txt5 = ctx['category'].name; let txt5 = ctx['category'].name;
c_block15[i1] = withKey(block16([attr6, attr7, txt5]), key1); c_block15[i1] = withKey(block16([attr6, attr7, txt5]), key1);
} }
@@ -277,7 +277,7 @@ exports[`misc other complex template 1`] = `
const b15 = list(c_block15); const b15 = list(c_block15);
b14 = block14([], [b15]); b14 = block14([], [b15]);
} }
let attr8 = ctx['search'].value; let attr8 = new String(ctx['search'].value);
let hdlr4 = [ctx['updateFilter'], ctx]; let hdlr4 = [ctx['updateFilter'], ctx];
let hdlr5 = [ctx['updateFilter'], ctx]; let hdlr5 = [ctx['updateFilter'], ctx];
let hdlr6 = [ctx['clearSearch'], ctx]; let hdlr6 = [ctx['clearSearch'], ctx];
@@ -291,7 +291,7 @@ exports[`misc other complex template 1`] = `
if (!ctx['trigger'].manual&&ctx['trigger'].project_id===ctx['project'].id&&ctx['trigger'].category_id===ctx['options'].active_category_id) { if (!ctx['trigger'].manual&&ctx['trigger'].project_id===ctx['project'].id&&ctx['trigger'].category_id===ctx['options'].active_category_id) {
let attr9 = \`trigger_\${ctx['trigger'].id}\`; let attr9 = \`trigger_\${ctx['trigger'].id}\`;
let attr10 = \`trigger_\${ctx['trigger'].id}\`; let attr10 = \`trigger_\${ctx['trigger'].id}\`;
let attr11 = ctx['options'].trigger_display[ctx['trigger'].id]; let attr11 = new Boolean(ctx['options'].trigger_display[ctx['trigger'].id]);
let attr12 = ctx['trigger'].id; let attr12 = ctx['trigger'].id;
let hdlr7 = [ctx['updateTriggerDisplay'], ctx]; let hdlr7 = [ctx['updateTriggerDisplay'], ctx];
let attr13 = \`trigger_\${ctx['trigger'].id}\`; let attr13 = \`trigger_\${ctx['trigger'].id}\`;
@@ -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);
+36
View File
@@ -429,6 +429,42 @@ describe("special cases for some specific html attributes/properties", () => {
expect(input.value).toBe("potato"); expect(input.value).toBe("potato");
}); });
test("input with t-att-value (patching with same value", () => {
// render input with initial value
const template = `<input t-att-value="v"/>`;
const bnode1 = renderToBdom(template, { v: "zucchini" });
const fixture = makeTestFixture();
mount(bnode1, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("zucchini");
// change value manually in input, to simulate user input
input.value = "tomato";
expect(input.value).toBe("tomato");
const bnode2 = renderToBdom(template, { v: "zucchini" });
patch(bnode1, bnode2);
expect(input.value).toBe("zucchini");
});
test("input, type checkbox, with t-att-checked (patching with same value", () => {
// render input with initial value
const template = `<input type="checkbox" t-att-checked="v"/>`;
const bnode1 = renderToBdom(template, { v: true });
const fixture = makeTestFixture();
mount(bnode1, fixture);
const input = fixture.querySelector("input")!;
expect(input.checked).toBe(true);
// change checked manually in input, to simulate user input
input.checked = false;
expect(input.checked).toBe(false);
const bnode2 = renderToBdom(template, { v: true });
patch(bnode1, bnode2);
expect(input.checked).toBe(true);
});
test("input of type checkbox with t-att-indeterminate", () => { test("input of type checkbox with t-att-indeterminate", () => {
const template = `<input type="checkbox" t-att-indeterminate="v"/>`; const template = `<input type="checkbox" t-att-indeterminate="v"/>`;
const bnode1 = renderToBdom(template, { v: true }); const bnode1 = renderToBdom(template, { v: true });
+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]);
@@ -101,6 +101,50 @@ exports[`basics simple catchError 2`] = `
}" }"
`; `;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: sync hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped out of dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped outside dev mode: sync hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 1`] = ` exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -131,19 +175,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
) { ) {
@@ -1309,3 +1340,16 @@ exports[`errors and promises errors in rerender 1`] = `
} }
}" }"
`; `;
exports[`errors and promises wrapped errors in async code are correctly caught 1`] = `
"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();
}
}"
`;
@@ -43,7 +43,7 @@ exports[`list of components crash on duplicate key in dev mode 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 { prepareList, withKey } = helpers; let { prepareList, OwlError, withKey } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true); const comp1 = app.createComponent(\`Child\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
@@ -53,7 +53,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) { for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1]; ctx[\`item\`] = v_block1[i1];
const key1 = 'child'; const key1 = 'child';
if (keys1.has(key1)) { throw new Error(\`Got duplicate key in t-foreach: \${key1}\`)} if (keys1.has(key1)) { throw new OwlError(\`Got duplicate key in t-foreach: \${key1}\`)}
keys1.add(key1); keys1.add(key1);
const props1 = {}; const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx); helpers.validateProps(\`Child\`, props1, ctx);
@@ -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();
+147 -27
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,11 +14,12 @@ import {
import { import {
logStep, logStep,
makeTestFixture, makeTestFixture,
nextMicroTick,
nextTick, nextTick,
nextMicroTick,
snapshotEverything, snapshotEverything,
useLogLifecycle, useLogLifecycle,
} from "../helpers"; } from "../helpers";
import { OwlError } from "../../src/runtime/error_handling";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -159,16 +160,17 @@ describe("errors and promises", () => {
static template = xml`<div><t t-esc="this.will.crash"/></div>`; static template = xml`<div><t t-esc="this.will.crash"/></div>`;
} }
let error: Error; let error: OwlError;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e as Error; error = e as OwlError;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp); expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
}); });
@@ -183,14 +185,15 @@ describe("errors and promises", () => {
} }
} }
let error: Error; let error: OwlError;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e as Error; error = e as OwlError;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe("boom"); expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe("boom");
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
@@ -264,6 +267,30 @@ describe("errors and promises", () => {
expect(error!.message).toBe("Tokenizer error: could not tokenize `{ 'invalid: 5 }`"); expect(error!.message).toBe("Tokenizer error: could not tokenize `{ 'invalid: 5 }`");
}); });
test("wrapped errors in async code are correctly caught", async () => {
class Root extends Component {
static template = xml`<div>abc</div>`;
setup() {
onWillStart(async () => {
await Promise.resolve();
throw new Error("boom in onWillStart");
});
}
}
let error: any;
try {
await mount(Root, fixture, { test: true });
} catch (e) {
error = e;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(
`The following error occurred in onWillStart: "boom in onWillStart"`
);
await new Promise((r) => setTimeout(r, 0)); // wait for the rejection event to bubble
});
test("an error in willPatch call will reject the render promise", async () => { test("an error in willPatch call will reject the render promise", async () => {
class Root extends Component { class Root extends Component {
static template = xml`<div><t t-esc="val"/></div>`; static template = xml`<div><t t-esc="val"/></div>`;
@@ -320,16 +347,17 @@ describe("errors and promises", () => {
static components = { Child }; static components = { Child };
} }
let error: Error; let error: OwlError;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e as Error; error = e as OwlError;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp); expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
@@ -339,7 +367,7 @@ describe("errors and promises", () => {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`; static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false; flag = false;
setup() { setup() {
onError((e) => (error = e)); onError(({ cause }) => (error = cause));
} }
} }
@@ -366,16 +394,17 @@ describe("errors and promises", () => {
static components = { Child }; static components = { Child };
} }
let error: Error; let error: OwlError;
try { try {
await mount(Parent, fixture); await mount(Parent, fixture);
} catch (e) { } catch (e) {
error = e as Error; error = e as OwlError;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error!.message).toMatch(regexp); expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0); expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
@@ -482,6 +511,96 @@ describe("can catch errors", () => {
); );
}); });
test("Errors in owl lifecycle are wrapped in dev mode: sync hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(`The following error occurred in onMounted: "test error"`);
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(async () => {
await nextMicroTick();
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(`The following error occurred in onWillStart: "test error"`);
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(async () => {
await nextMicroTick();
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => { test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component { class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`; static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
@@ -1180,8 +1299,8 @@ describe("can catch errors", () => {
class Catch extends Component { class Catch extends Component {
static template = xml`<t t-slot="default" />`; static template = xml`<t t-slot="default" />`;
setup() { setup() {
onError((error) => { onError(({ cause }) => {
this.props.onError(error); this.props.onError(cause);
}); });
} }
} }
@@ -1251,9 +1370,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 +1395,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 +1433,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");
}); });
+1 -1
View File
@@ -653,7 +653,7 @@ describe("hooks", () => {
try { try {
await mount(MyComponent, fixture); await mount(MyComponent, fixture);
} catch (e: any) { } catch (e: any) {
expect(e.message).toBe("Intentional error"); expect(e.cause.message).toBe("Intentional error");
} }
// no console.error because the error has been caught in this test // no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0); expect(console.error).toHaveBeenCalledTimes(0);
+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();
+5 -3
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../../src/runtime/error_handling";
import { Component, mount, onMounted, useState, xml } from "../../src"; import { Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
@@ -346,16 +347,17 @@ describe("style and class handling", () => {
static template = xml`<Child class="'a'"/>`; static template = xml`<Child class="'a'"/>`;
static components = { Child }; static components = { Child };
} }
let error: Error; let error: OwlError;
try { try {
await mount(ParentWidget, fixture); await mount(ParentWidget, fixture);
} catch (e) { } catch (e) {
error = e as Error; error = e as OwlError;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp); expect(error!.cause.message).toMatch(regexp);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(mockConsoleWarn).toBeCalledTimes(1); expect(mockConsoleWarn).toBeCalledTimes(1);
}); });
+2 -1
View File
@@ -19,6 +19,7 @@ import { helpers } from "../src/runtime/template_helpers";
import { TemplateSet, globalTemplates } from "../src/runtime/template_set"; import { TemplateSet, globalTemplates } from "../src/runtime/template_set";
import { BDom } from "../src/runtime/blockdom"; import { BDom } from "../src/runtime/blockdom";
import { compile } from "../src/compiler"; import { compile } from "../src/compiler";
import { OwlError } from "../src/runtime/error_handling";
const mount = blockDom.mount; const mount = blockDom.mount;
@@ -221,7 +222,7 @@ export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, v
afterEach(() => { afterEach(() => {
if (steps.length) { if (steps.length) {
steps.splice(0); steps.splice(0);
throw new Error("Remaining steps! Should be checked by a .toBeLogged() assertion!"); throw new OwlError("Remaining steps! Should be checked by a .toBeLogged() assertion!");
} }
}); });
+6 -4
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../../src/runtime/error_handling";
import { import {
App, App,
Component, Component,
@@ -499,7 +500,7 @@ describe("Portal", () => {
</div>`; </div>`;
state = { error: false }; state = { error: false };
setup() { setup() {
onError((e) => (error = e)); onError(({ cause }) => (error = cause));
} }
} }
addOutsideDiv(fixture); addOutsideDiv(fixture);
@@ -958,14 +959,15 @@ describe("Portal: Props validation", () => {
</t> </t>
</div>`; </div>`;
} }
let error: Error; let error: OwlError;
try { try {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
} catch (e) { } catch (e) {
error = e as Error; error = e as OwlError;
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe(`' ' is not a valid selector`); expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe(`' ' is not a valid selector`);
}); });
test("target must be a valid selector 2", async () => { test("target must be a valid selector 2", async () => {
+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);
});
});