Compare commits

..

14 Commits

Author SHA1 Message Date
Géry Debongnie b1e79677cb wip 2021-11-20 10:45:30 +01:00
Géry Debongnie 89d6b8b7dd wip 2021-11-20 10:25:37 +01:00
Géry Debongnie 9c2523e3ce wip 2021-11-20 10:25:37 +01:00
Géry Debongnie d8201b8955 add possibility to deep rendering 2021-11-20 10:25:37 +01:00
Géry Debongnie dfead2836e big change: shallow render
With this commit, component only render child
components if they have different props (shallow
equality). Otherwise, we trust the reactivity
system to make sure that all impacted components
are updated
2021-11-20 10:25:37 +01:00
Géry Debongnie 0e0ac5329a [REF] utils: move batched from reactivity to utils 2021-11-20 10:25:36 +01:00
Lucas Perais (lpe) efd934d2b1 [FIX] tools: adapt playground to owl 2 2021-11-19 16:11:28 +01:00
Lucas Perais (lpe) dabc24cee3 [FIX] index, reactivity: export reactive function in index 2021-11-19 16:11:28 +01:00
Samuel Degueldre 7f49796a07 [IMP] misc: update typescript to 4.5.2 2021-11-19 14:42:37 +01:00
Géry Debongnie 757dffefac [IMP] components: rename onRender->onWillRender, add onRendered 2021-11-19 13:26:44 +01:00
Samuel Degueldre bdfe058279 [IMP] reactivity: overhaul reactivity system
This commit makes the reactivity system more fine grained and makes it
more eager to stop observing keys or objects when they are modified,
this results in fewer "false positive" notifications.
2021-11-19 13:20:37 +01:00
Bruno Boi 345c44b952 fixup! [IMP] svg namespace support 2021-11-19 12:47:51 +01:00
Bruno Boi ea74739d46 [IMP] svg namespace support 2021-11-19 11:55:18 +01:00
Géry Debongnie 93b53d8017 [FIX] remove cyclic dependency, improve error typing (#982) 2021-11-18 10:44:49 +01:00
53 changed files with 2729 additions and 2052 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ removed after.
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- new `useEffect` hook
- new `onDestroyed` and `onRender` hooks
- new `onDestroyed`, `onWillRender` and `onRendered` hooks
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
+1 -1
View File
@@ -56,7 +56,7 @@
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^27.0.5",
"typescript": "3.9.6",
"typescript": "4.5.2",
"uglify-es": "^3.3.9"
},
"jest": {
+20 -9
View File
@@ -116,6 +116,7 @@ interface IntermediateTree {
forceRef?: boolean;
refIdx?: number;
refN: number;
currentNS: string | null;
}
function buildTree(
@@ -124,9 +125,10 @@ function buildTree(
domParentTree: IntermediateTree | null = null
): IntermediateTree {
switch (node.nodeType) {
case 1: {
case Node.ELEMENT_NODE: {
// HTMLElement
let isActive = false;
let currentNS = parent && parent.currentNS;
const tagName = (node as Element).tagName;
let el: Node | undefined = undefined;
const info: DynamicInfo[] = [];
@@ -143,11 +145,18 @@ function buildTree(
el = document.createTextNode("");
isActive = true;
}
if (!el) {
el = document.createElement(tagName);
const attrs = (node as Element).attributes;
const ns = attrs.getNamedItem("block-ns");
if (ns) {
attrs.removeNamedItem("block-ns");
currentNS = ns.value;
}
if (el instanceof HTMLElement) {
const attrs = (node as Element).attributes;
if (!el) {
el = currentNS
? document.createElementNS(currentNS, tagName)
: document.createElement(tagName);
}
if (el instanceof Element) {
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
const attrValue = attrs[i].value;
@@ -193,13 +202,14 @@ function buildTree(
el,
info,
refN: isActive ? 1 : 0,
currentNS,
};
if (node.firstChild) {
const childNode = node.childNodes[0];
if (
node.childNodes.length === 1 &&
childNode.nodeType === 1 &&
childNode.nodeType === Node.ELEMENT_NODE &&
(childNode as Element).tagName.startsWith("block-child-")
) {
const tagName = (childNode as Element).tagName;
@@ -227,11 +237,11 @@ function buildTree(
}
return tree;
}
case 3:
case 8: {
case Node.TEXT_NODE:
case Node.COMMENT_NODE: {
// text node or comment node
const el =
node.nodeType === 3
node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.textContent!)
: document.createComment(node.textContent!);
return {
@@ -241,6 +251,7 @@ function buildTree(
el,
info: [],
refN: 0,
currentNS: null,
};
}
}
+6 -2
View File
@@ -497,6 +497,10 @@ export class CodeGenerator {
}
// attributes
const attrs: { [key: string]: string } = {};
if (ast.ns) {
// specific namespace uri
attrs["block-ns"] = ast.ns;
}
for (let key in ast.attrs) {
if (key.startsWith("t-attf")) {
let expr = interpolate(ast.attrs[key]);
@@ -944,7 +948,7 @@ export class CodeGenerator {
let propString = propStr;
if (ast.dynamicProps) {
if (!props.length) {
propString = `${compileExpr(ast.dynamicProps)}`;
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`;
} else {
propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
}
@@ -1008,7 +1012,7 @@ export class CodeGenerator {
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
}
const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`;
let blockExpr = `component(${blockArgs})`;
let blockExpr = `component(${blockArgs}${hasSlot ? ", true" : ""})`;
if (Object.keys(extraArgs).length) {
this.shouldDefineAssign = true;
const content = Object.keys(extraArgs).map((k) => `${k}: ${extraArgs[k]}`);
+12 -9
View File
@@ -49,6 +49,7 @@ export interface ASTDomNode {
shouldTrim: boolean;
shouldNumberize: boolean;
} | null;
ns: string | null;
}
export interface ASTMulti {
@@ -171,11 +172,12 @@ export type AST =
// -----------------------------------------------------------------------------
interface ParsingContext {
inPreTag: boolean;
inSVG: boolean;
}
export function parse(xml: string | Node): AST {
const node = xml instanceof Element ? xml : parseXML(`<t>${xml}</t>`).firstChild!;
const ctx = { inPreTag: false };
const ctx = { inPreTag: false, inSVG: false };
const ast = parseNode(node, ctx);
if (!ast) {
return { type: ASTType.Text, value: "" };
@@ -240,7 +242,7 @@ const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
if (node.nodeType === 3) {
if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || "";
if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) {
@@ -250,7 +252,7 @@ function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null
}
return { type: ASTType.Text, value };
} else if (node.nodeType === 8) {
} else if (node.nodeType === Node.COMMENT_NODE) {
return { type: ASTType.Comment, value: node.textContent || "" };
}
return null;
@@ -289,18 +291,18 @@ const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const { tagName } = node;
let dynamicTag = null;
if (node.hasAttribute("t-tag")) {
dynamicTag = node.getAttribute("t-tag");
node.removeAttribute("t-tag");
}
const dynamicTag = node.getAttribute("t-tag");
node.removeAttribute("t-tag");
if (tagName === "t" && !dynamicTag) {
return null;
}
const children: AST[] = [];
if (tagName === "pre") {
ctx = { inPreTag: true };
ctx.inPreTag = true;
}
const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
@@ -381,6 +383,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
ref,
content: children,
model,
ns,
};
}
+5 -2
View File
@@ -1,6 +1,8 @@
import type { Env } from "../app/app";
import type { ComponentNode } from "./component_node";
export type Props = { [key: string]: any };
// -----------------------------------------------------------------------------
// Component Class
// -----------------------------------------------------------------------------
@@ -8,6 +10,7 @@ import type { ComponentNode } from "./component_node";
export class Component {
static template: string = "";
static style: string = "";
static props?: any;
props: any;
env: Env;
@@ -25,7 +28,7 @@ export class Component {
setup() {}
render(): Promise<void> {
return this.__owl__.render();
render(deep: boolean = false): Promise<void> {
return this.__owl__.render(deep);
}
}
+75 -16
View File
@@ -1,6 +1,9 @@
import type { App, Env } from "../app/app";
import { BDom, VNode } from "../blockdom";
import { Component } from "./component";
import { clearReactivesForCallback, Reactive, reactive } from "../reactivity";
import { batched, Callback } from "../utils";
import { Component, Props } from "./component";
import { fibersInError, handleError } from "./error_handling";
import {
Fiber,
makeChildFiber,
@@ -8,20 +11,68 @@ import {
MountFiber,
MountOptions,
RootFiber,
__internal__destroyed,
__internal__destroyed
} from "./fibers";
import { handleError, fibersInError } from "./error_handling";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status";
import { applyStyles } from "./style";
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
if (!batchedRenderFunctions.has(currentNode!)) {
batchedRenderFunctions.set(
currentNode!,
batched(() => currentNode!.render())
);
}
const render = batchedRenderFunctions.get(currentNode!)!;
const reactiveState = reactive(state, render);
// manual implementation of onWillUnmount to break cyclic dependency
currentNode!.willUnmount.unshift( clearReactivesForCallback.bind(null, render))
return reactiveState;
}
// -----------------------------------------------------------------------------
// component function (used in compiled template code)
// -----------------------------------------------------------------------------
function arePropsDifferent(props1: Props, props2: Props): boolean {
for (let k in props1) {
if (props1[k] !== props2[k]) {
return true;
}
}
return false;
}
export function component(
name: string | typeof Component,
props: any,
key: string,
ctx: ComponentNode,
parent: any
parent: any,
hasSlots: boolean = false
): ComponentNode {
console.warn('asdf')
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
@@ -39,7 +90,11 @@ export function component(
const parentFiber = ctx.fiber!;
if (node) {
node.updateAndRender(props, parentFiber);
console.warn('coucou');
if (hasSlots || parentFiber.deep || arePropsDifferent(node.component.props, props)) {
console.warn('coucou3');
node.updateAndRender(props, parentFiber);
}
} else {
// new component
let C;
@@ -61,18 +116,14 @@ export function component(
}
// -----------------------------------------------------------------------------
// Component VNode
// Component VNode class
// -----------------------------------------------------------------------------
let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null {
return currentNode;
}
type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = any> implements VNode<ComponentNode> {
export class ComponentNode<T extends typeof Component = typeof Component>
implements VNode<ComponentNode>
{
el?: HTMLElement | Text | undefined;
app: App;
fiber: Fiber | null = null;
@@ -104,6 +155,9 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
// if (props) {
// props = useState(props);
// }
this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
if (C.style) {
@@ -127,7 +181,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
try {
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
handleError(this, e as Error);
handleError({ node: this, error: e });
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) {
@@ -135,7 +189,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
}
}
async render() {
async render(deep: boolean = false) {
let fiber = this.fiber;
if (fiber && !fiber.bdom && !fibersInError.has(fiber)) {
return fiber.root.promise;
@@ -145,6 +199,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
return;
}
fiber = makeRootFiber(this);
fiber.deep = deep;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
if (this.status === STATUS.DESTROYED) {
@@ -161,7 +216,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
fiber.bdom = this.renderFn();
fiber.root.counter--;
} catch (e) {
handleError(this, e as Error);
handleError({ node: this, error: e });
}
}
@@ -239,6 +294,10 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
}
patch() {
if (!this.fiber) {
// component was not rendered => no need to do anything
return;
}
this.bdom!.patch(this!.fiber!.bdom!, false);
this.fiber!.appliedToDom = true;
this.fiber = null;
+11 -15
View File
@@ -1,10 +1,11 @@
import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers";
export const fibersInError: WeakMap<Fiber, Error> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: Error) => void)[]> = new WeakMap();
// Maps fibers to thrown errors
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: Error, isFirstRound = false): boolean {
function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
if (!node) {
return false;
}
@@ -25,7 +26,7 @@ function _handleError(node: ComponentNode | null, error: Error, isFirstRound = f
h(error);
propagate = false;
} catch (e) {
error = e as Error;
error = e;
}
}
@@ -38,17 +39,12 @@ function _handleError(node: ComponentNode | null, error: Error, isFirstRound = f
}
}
export function handleError(entity: ComponentNode | Fiber, error: Error) {
let node: ComponentNode;
let fiber: Fiber;
// soft type check on Fiber
if ("node" in entity) {
fiber = entity;
node = entity.node;
} else {
node = entity;
fiber = entity.fiber!;
}
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
export function handleError(params: ErrorParams) {
const error = params.error;
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
fibersInError.set(fiber.root, error);
const handled = _handleError(node, error, true);
+3 -2
View File
@@ -67,6 +67,7 @@ export class Fiber {
parent: Fiber | null;
children: Fiber[] = [];
appliedToDom = false;
deep: boolean = false;
constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node;
@@ -159,7 +160,7 @@ export class RootFiber extends Fiber {
// unregistering the fiber
node.fiber = null;
} catch (e) {
if (!handleError(current || this, e)) {
if (!handleError({ fiber: current || this, error: e })) {
this.reject(e);
}
}
@@ -206,7 +207,7 @@ export class MountFiber extends RootFiber {
}
node.fiber = null;
} catch (e) {
if (!handleError(current as Fiber, e)) {
if (!handleError({ fiber: current as Fiber, error: e })) {
this.reject(e);
}
}
+11 -1
View File
@@ -40,7 +40,7 @@ export function onDestroyed(fn: () => Promise<void> | void | any) {
node.destroyed.push(fn);
}
export function onRender(fn: () => void | any) {
export function onWillRender(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
@@ -49,6 +49,16 @@ export function onRender(fn: () => void | any) {
};
}
export function onRendered(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
const result = renderFn();
fn();
return result;
};
}
export function onError(fn: (error: Error) => void | any) {
const node = getCurrent()!;
let handlers = nodeErrorHandlers.get(node);
+9 -10
View File
@@ -26,26 +26,23 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass:
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
// message: { type: String, default: "hello" };
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
const ComponentClass =
typeof name !== "string" ? name : parent.constructor.components[name as string];
const ComponentClass = (
typeof name !== "string" ? name : parent.constructor.components[name]
) as typeof Component;
applyDefaultProps(props, ComponentClass);
const propsDef = (<any>ComponentClass).props;
const propsDef = ComponentClass.props;
if (propsDef instanceof Array) {
// list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) {
const propName = propsDef[i];
for (const propName of propsDef) {
if (propName[propName.length - 1] === "?") {
// optional prop
break;
}
if (!(propName in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${ComponentClass.name}')`);
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
}
}
for (let key in props) {
@@ -67,7 +64,9 @@ export const validateProps = function (name: string | typeof Component, props: a
try {
isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message
})`;
throw e;
}
if (!isValid) {
+4 -2
View File
@@ -34,6 +34,7 @@ import type { AppConfig } from "./app/app";
import { App } from "./app/app";
import { Component } from "./component/component";
import { getCurrent } from "./component/component_node";
export { useState } from "./component/component_node";
export { App, Component };
@@ -55,7 +56,7 @@ export { status } from "./component/status";
export { Portal } from "./portal";
export { Memo } from "./memo";
export { css, xml } from "./tags";
export { useState } from "./reactivity";
export { reactive } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils";
@@ -66,7 +67,8 @@ export {
onWillUpdateProps,
onWillPatch,
onPatched,
onRender,
onWillRender,
onRendered,
onDestroyed,
onError,
} from "./component/lifecycle_hooks";
+172 -259
View File
@@ -1,132 +1,26 @@
import { ComponentNode, getCurrent } from "./component/component_node";
import { onWillUnmount } from "./component/lifecycle_hooks";
import { Callback } from "./utils";
type Observer = ComponentNode | Function;
type Atom = any; // proxy linked to a unique observer and source
type Source = any; // trackable that is not an atom
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
const TARGET = Symbol("Target");
// Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
const sourceAtoms: WeakMap<Source, Map<any, ObserverSet>> = new WeakMap();
const observerSourceAtom: WeakMap<Observer, Map<any, Atom>> = new WeakMap();
type ObjectKey = string | number | symbol;
type Target = object;
export type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
const SOURCE = Symbol("source");
const OBSERVER = Symbol("observer");
const KEYS = Symbol("keys");
const ROOT = Symbol("root");
const SEED = Symbol("Seed");
export function observe(source: any, observer: Observer): [Atom, Function] {
if (!isTrackable(source)) {
throw new Error("First argument is not trackable");
}
const unregisterObserver = registerObserver(observer);
const newAtom = atom(source, observer);
return [newAtom, unregisterObserver];
}
function atom(source: any, observer: Observer) {
return _atom(source, observer, true);
}
function _atom(source: any, observer: Observer, seed = false) {
if (isTrackable(source) && observerSourceAtom.has(observer)) {
source = (source as any)[SOURCE] || source;
const oldAtom = observerSourceAtom.get(observer)!.get(source);
if (oldAtom) {
if (seed) {
oldAtom[SEED] = true;
}
return oldAtom;
}
if (!sourceAtoms.get(source)) {
sourceAtoms.set(source, new Map([[ROOT, new ObserverSet()]]));
}
const newAtom = createAtom(source, observer);
if (seed) {
newAtom[SEED] = true;
}
observerSourceAtom.get(observer)!.set(source, newAtom);
sourceAtoms.get(source)!.get(ROOT)!.add(newAtom);
return newAtom;
}
return source;
}
function createAtom(source: Source, observer: Observer): Atom {
const keys: Set<any> = new Set();
let seed: boolean = false;
const newAtom: Atom = new Proxy(source as any, {
set(target: any, key: any, value: any): boolean {
if (key === SEED) {
seed = value;
return true;
}
if (!(key in target)) {
target[key] = value;
notify(sourceAtoms.get(source)!.get(ROOT)!);
return true;
}
const current = target[key];
if (current !== value) {
target[key] = value;
const observerSet = sourceAtoms.get(source)!.get(key);
if (observerSet) {
const clean = isTrackable(current);
notify(observerSet, clean);
}
}
return true;
},
deleteProperty(target: any, key: string): boolean {
if (key in target) {
const current = target[key];
delete target[key];
// notify source observers
const clean = isTrackable(current);
notify(sourceAtoms.get(source)!.get(ROOT)!, clean);
const atoms = sourceAtoms.get(source)!;
if (atoms.has(key)) {
// clear source-key observers
for (const atom of atoms.get(key)!) {
atom[KEYS].delete(key);
}
atoms.delete(key);
}
}
return true;
},
get(target: any, key: any, proxy: any): any {
switch (key) {
case OBSERVER:
return observer;
case SOURCE:
return source;
case KEYS:
return keys;
case SEED:
return seed;
default:
const value = target[key];
// register observer to source-key
if (!keys.has(key) && observerSourceAtom.has(observer)) {
const atoms = sourceAtoms.get(source)!;
if (!atoms.has(key)) {
atoms.set(key, new ObserverSet());
}
atoms.get(key)!.add(newAtom);
keys.add(key);
}
//
return _atom(value, observer);
}
},
});
return newAtom;
}
function isTrackable(value: any): boolean {
/**
* Checks whether a given value can be made into a reactive object.
*
* @param value the value to check
* @returns whether the value can be made reactive
*/
function canBeMadeReactive(value: any): boolean {
return (
value !== null &&
typeof value === "object" &&
value !== null &&
!(value instanceof Date) &&
!(value instanceof Promise) &&
!(value instanceof String) &&
@@ -134,144 +28,163 @@ function isTrackable(value: any): boolean {
);
}
function registerObserver(observer: Observer) {
if (!observerSourceAtom.get(observer)) {
observerSourceAtom.set(observer, new Map());
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
/**
* Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target.
*
* @param target the target whose key should be observed
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
* or deletion)
* @param callback the function to call when the key changes
*/
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
return unregisterObserverAtoms.bind(null, observer, false);
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
}
keyToCallbacks.get(key)!.add(callback);
}
function unregisterObserverAtoms(observer: Observer, keepSeeds: boolean) {
const observerAtoms = observerSourceAtom.get(observer)!;
for (const [source, atom] of observerAtoms) {
if (keepSeeds && atom[SEED]) {
continue;
}
observerAtoms.delete(source);
const atoms = sourceAtoms.get(source)!;
atoms.get(ROOT)!.delete(atom);
for (const key of atom[KEYS]) {
atoms.get(key)!.delete(atom);
}
}
if (!keepSeeds) {
observerSourceAtom.delete(observer);
}
}
export function useState(state: any): Atom {
const node = getCurrent()!;
const [newAtom, unregisterObserver] = observe(state, node);
onWillUnmount(() => unregisterObserver());
return newAtom;
}
class ObserverSet {
nodeAntichain: Antichain = new Antichain();
callbackSet: Set<Atom> = new Set();
add(atom: Atom) {
if (atom[OBSERVER] instanceof ComponentNode) {
this.nodeAntichain.add(atom);
} else {
this.callbackSet.add(atom);
}
return this;
}
delete(atom: Atom) {
if (atom[OBSERVER] instanceof ComponentNode) {
return this.nodeAntichain.delete(atom);
} else {
return this.callbackSet.delete(atom);
}
}
union(other: ObserverSet) {
for (const atom of other.nodeAntichain) {
this.nodeAntichain.add(atom);
}
for (const callback of other.callbackSet) {
this.callbackSet.add(callback);
}
}
notify() {
for (const atom of this.nodeAntichain) {
// an observer cannot be found twice here: <= in add is based on observers
atom[OBSERVER].render();
}
const called = new Set();
for (const atom of this.callbackSet) {
const observer = atom[OBSERVER];
if (!called.has(observer)) {
observer();
}
called.add(observer);
}
}
*[Symbol.iterator]() {
yield* this.nodeAntichain;
yield* this.callbackSet;
}
}
// Need to optimize this!
function isLessOrEqual(node1: ComponentNode, node2: ComponentNode) {
let current: any = node1;
if (current.level <= node2.level) {
return false;
}
do {
if (current === node2) {
return true;
}
current = current.parent;
} while (current);
return false;
}
// set of atoms linked to observers of type ComponentNode
class Antichain extends Set<Atom> {
level?: number;
add(atom: Atom) {
const node = atom[OBSERVER];
if (this.level === node.level || this.level === undefined) {
super.add(atom);
this.level = node.level;
return this;
}
let willAdd = false;
for (const atom2 of this) {
const node2 = atom2[OBSERVER];
if (!willAdd && isLessOrEqual(node, node2)) {
return this;
} else if (isLessOrEqual(node2, node)) {
super.delete(atom2);
willAdd = true;
}
}
super.add(atom);
this.level = NaN;
return this;
}
}
let toNotify: ObserverSet | null = null;
let toClean: Set<Observer> = new Set();
async function notify(observers: ObserverSet, clean = false) {
if (clean) {
for (const atom of observers) {
toClean.add(atom[OBSERVER]);
}
}
if (toNotify) {
toNotify.union(observers);
/**
* Notify Reactives that are observing a given target that a key has changed on
* the target.
*
* @param target target whose Reactives should be notified that the target was
* changed.
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
* or deleted)
*/
function notifyReactives(target: Target, key: ObjectKey): void {
const keyToCallbacks = targetToKeysToCallbacks.get(target);
if (!keyToCallbacks) {
return;
}
toNotify = new ObserverSet();
toNotify.union(observers);
await Promise.resolve();
for (const observer of toClean) {
unregisterObserverAtoms(observer, true);
const callbacks = keyToCallbacks.get(key);
if (!callbacks) {
return;
}
// Loop on copy because clearReactivesForCallback will modify the set in place
for (const callback of [...callbacks]) {
clearReactivesForCallback(callback);
callback();
}
toClean.clear();
toNotify.notify();
toNotify = null;
}
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
/**
* Clears all subscriptions of the Reactives associated with a given callback.
*
* @param callback the callback for which the reactives need to be cleared
*/
export function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
}
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const callbacks of observedKeys.values()) {
callbacks.delete(callback);
}
}
}
const reactiveCache = new WeakMap<Target, Map<Callback, Reactive>>();
/**
* Creates a reactive proxy for an object. Reading data on the reactive object
* subscribes to changes to the data. Writing data on the object will cause the
* notify callback to be called if there are suscriptions to that data. Nested
* objects and arrays are automatically made reactive as well.
*
* Whenever you are notified of a change, all subscriptions are cleared, and if
* you would like to be notified of any further changes, you should go read
* the underlying data again. We assume that if you don't go read it again after
* being notified, it means that you are no longer interested in that data.
*
* Subscriptions:
* + Reading a property on an object will subscribe you to changes in the value
* of that property.
* + Accessing an object keys (eg with Object.keys or with `for..in`) will
* subscribe you to the creation/deletion of keys. Checking the presence of a
* key on the object with 'in' has the same effect.
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
* This is a choice that was made because changing a key's value will trigger
* this trap and we do not want to subscribe by writes. This also means that
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
*
* @param target the object for which to create a reactive proxy
* @param callback the function to call when an observed property of the
* reactive has changed
* @returns a proxy that tracks changes to it
*/
export function reactive<T extends Target>(target: T, callback: Callback): Reactive<T> {
if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`);
}
const originalTarget = (target as Reactive)[TARGET];
if (originalTarget) {
return reactive(originalTarget, callback);
}
if (!reactiveCache.has(target)) {
reactiveCache.set(target, new Map());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const proxy = new Proxy(target, {
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
if (key === TARGET) {
return target;
}
observeTargetKey(target, key, callback);
const value = Reflect.get(target, key, proxy);
if (!canBeMadeReactive(value)) {
return value;
}
return reactive(value, callback);
},
set(target, key, value, proxy) {
const isNewKey = !Object.hasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, proxy);
const ret = Reflect.set(target, key, value, proxy);
if (isNewKey) {
notifyReactives(target, KEYCHANGES);
}
// While Array length may trigger the set trap, it's not actually set by this
// method but is updated behind the scenes, and the trap is not called with the
// new value. We disable the "same-value-optimization" for it because of that.
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
notifyReactives(target, key);
}
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
notifyReactives(target, KEYCHANGES);
notifyReactives(target, key);
return ret;
},
ownKeys(target) {
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.ownKeys(target);
},
has(target, key) {
// TODO: this observes all key changes instead of only the presence of the argument key
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.has(target, key);
},
});
reactivesForTarget.set(callback, proxy);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
+26
View File
@@ -1,3 +1,29 @@
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;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
}
};
}
export class EventBus extends EventTarget {
trigger(name: string, payload?: any) {
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
@@ -1,5 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reactivity: useState concurrent renderings 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['context'][ctx['props'].key].n;
let d2 = ctx['state'].x;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
"function anonymous(bdom, helpers
) {
+72
View File
@@ -0,0 +1,72 @@
import { createBlock, mount } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
const XHTML_URI = "http://www.w3.org/1999/xhtml";
const SVG_URI = "http://www.w3.org/2000/svg";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("namespace", () => {
test("default namespace is xhtml", () => {
const block = createBlock(`<tag/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
});
test("namespace can be changed with block-ns", () => {
const block = createBlock(`<tag block-ns="${SVG_URI}"/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
});
test("namespace is kept for children", () => {
const block = createBlock(
`<parent block-ns="${SVG_URI}"><child><subchild/></child><child/></parent>`
);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<parent><child><subchild></subchild></child><child></child></parent>"
);
const parent = fixture.firstElementChild!;
const child1 = parent.firstElementChild!;
const subchild = child1.firstElementChild!;
const child2 = child1.nextElementSibling!;
expect(parent.namespaceURI).toBe(SVG_URI);
expect(child1.namespaceURI).toBe(SVG_URI);
expect(child2.namespaceURI).toBe(SVG_URI);
expect(subchild.namespaceURI).toBe(SVG_URI);
});
test("various namespaces in same block", () => {
const block = createBlock(`<none><one block-ns="one"/><two block-ns="two"/></none>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<none><one></one><two></two></none>");
const none = fixture.firstElementChild!;
const one = none.firstElementChild!;
const two = one.nextElementSibling!;
expect(none.namespaceURI).toBe(XHTML_URI);
expect(one.namespaceURI).toBe("one");
expect(two.namespaceURI).toBe("two");
});
});
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`properly support svg add proper namespace to g tags 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg add proper namespace to svg 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to g tags not added if already in svg namespace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><g/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to svg tags added even if already in svg namespace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg block-ns=\\"http://www.w3.org/2000/svg\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
+118 -1
View File
@@ -46,6 +46,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
});
});
@@ -73,6 +74,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
content: [],
ns: null,
});
});
@@ -86,6 +88,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
content: [{ type: ASTType.Text, value: "some text" }],
ns: null,
});
});
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{ type: ASTType.Text, value: "some text" },
{
@@ -108,6 +112,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "inside" }],
},
],
@@ -126,6 +131,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [],
},
{
@@ -136,6 +142,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [],
},
],
@@ -155,6 +162,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [],
},
],
@@ -170,10 +178,83 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "foo" }],
});
});
test("svg dom node", async () => {
expect(
parse(
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></svg>`
)
).toEqual({
attrs: {
height: "90px",
width: "100px",
},
content: [
{
attrs: {
cx: "50",
cy: "50",
fill: "yellow",
r: "4",
stroke: "green",
"stroke-width": "1",
},
content: [],
dynamicTag: null,
model: null,
ns: null,
on: {},
ref: null,
tag: "circle",
type: 2,
},
],
dynamicTag: null,
model: null,
ns: "http://www.w3.org/2000/svg",
on: {},
ref: null,
tag: "svg",
type: 2,
});
expect(
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
).toEqual({
attrs: {},
content: [
{
attrs: {
cx: "50",
cy: "50",
fill: "yellow",
r: "4",
stroke: "green",
"stroke-width": "1",
},
content: [],
dynamicTag: null,
model: null,
ns: null,
on: {},
ref: null,
tag: "circle",
type: 2,
},
],
dynamicTag: null,
model: null,
ns: "http://www.w3.org/2000/svg",
on: {},
ref: null,
tag: "g",
type: 2,
});
});
// ---------------------------------------------------------------------------
// t-esc
// ---------------------------------------------------------------------------
@@ -200,6 +281,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "" }],
});
});
@@ -221,6 +303,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "hey" }],
});
});
@@ -262,6 +345,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TOut, expr: "text", body: null }],
});
});
@@ -275,6 +359,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{ type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] },
],
@@ -294,6 +379,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{
type: ASTType.TIf,
@@ -321,6 +407,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
},
tElif: null,
@@ -397,6 +484,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{
type: ASTType.Text,
@@ -415,6 +503,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "elif" }],
},
},
@@ -427,6 +516,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{
type: ASTType.Text,
@@ -474,6 +564,7 @@ describe("qweb parser", () => {
dynamicTag: null,
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "ok" }],
},
],
@@ -493,6 +584,7 @@ describe("qweb parser", () => {
dynamicTag: null,
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "ok" }],
},
{ type: ASTType.Text, value: "abc" },
@@ -527,6 +619,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{
type: ASTType.TIf,
@@ -601,6 +694,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
memo: "",
@@ -646,6 +740,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
memo: "",
@@ -681,6 +776,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
},
@@ -715,6 +811,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "category.name", defaultValue: "" }],
},
memo: "",
@@ -736,6 +833,7 @@ describe("qweb parser", () => {
model: null,
tag: "div",
dynamicTag: null,
ns: null,
content: [
{
type: ASTType.TForEach,
@@ -781,6 +879,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
attrs: {},
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
memo: "",
@@ -888,6 +987,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [
{
type: ASTType.TCall,
@@ -925,6 +1025,7 @@ describe("qweb parser", () => {
on: { click: "add" },
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "Click" }],
});
});
@@ -1003,6 +1104,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
on: {},
ns: null,
},
{
type: ASTType.DomNode,
@@ -1013,6 +1115,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
on: {},
ns: null,
},
],
},
@@ -1203,6 +1306,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
},
});
@@ -1220,6 +1324,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
},
});
@@ -1238,6 +1343,7 @@ describe("qweb parser", () => {
on: {},
ref: "name",
model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
});
});
@@ -1251,6 +1357,7 @@ describe("qweb parser", () => {
on: {},
ref: "name",
model: null,
ns: null,
content: [
{ type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] },
],
@@ -1266,6 +1373,7 @@ describe("qweb parser", () => {
on: {},
ref: "name",
model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "body" }],
});
});
@@ -1309,7 +1417,8 @@ describe("qweb parser", () => {
model: null,
tag: "div",
dynamicTag: null,
type: 2,
type: ASTType.DomNode,
ns: null,
},
type: 16,
},
@@ -1339,6 +1448,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1357,6 +1467,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1375,6 +1486,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1394,6 +1506,7 @@ describe("qweb parser", () => {
ref: null,
tag: "textarea",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1412,6 +1525,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1430,6 +1544,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1448,6 +1563,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1472,6 +1588,7 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: "theTag",
model: null,
ns: null,
});
});
});
+25 -17
View File
@@ -1,22 +1,30 @@
import { renderToString, snapshotEverything } from "../helpers";
// NB: check the snapshots to see where the SVG namespaces are added
snapshotEverything();
describe("properly support svg", () => {
test.skip("add proper namespace to svg", () => {
// qweb.addTemplate(
// "test",
// `<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`
// );
// expect(renderToString(qweb, "test")).toBe(
// `<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
// );
test("add proper namespace to svg", () => {
const template = `<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`;
expect(renderToString(template)).toBe(
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
);
});
test.skip("add proper namespace to g tags", () => {
// this is necessary if one wants to use components in a svg
// qweb.addTemplate(
// "test",
// `<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`
// );
// expect(renderToString(qweb, "test")).toBe(
// `<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
// );
test("add proper namespace to g tags", () => {
const template = `<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`;
expect(renderToString(template)).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
);
});
test("namespace to g tags not added if already in svg namespace", () => {
const template = `<svg><g/></svg>`;
expect(renderToString(template)).toBe(`<svg><g></g></svg>`);
});
test("namespace to svg tags added even if already in svg namespace", () => {
const template = `<svg><svg/></svg>`;
expect(renderToString(template)).toBe(`<svg><svg></svg></svg>`);
});
});
@@ -1020,7 +1020,7 @@ exports[`basics update props of component without concrete own node 3`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['childProps'].key;
let b2 = toggler(tKey_1, component(\`Child\`, ctx['childProps'], tKey_1 + key + \`__2\`, node, ctx));
let b2 = toggler(tKey_1, component(\`Child\`, Object.assign({}, ctx['childProps']), tKey_1 + key + \`__2\`, node, ctx));
return block1([], [b2]);
}
}"
@@ -1192,7 +1192,7 @@ exports[`support svg components add proper namespace to svg 1`] = `
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<g><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`);
let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
@@ -1206,7 +1206,7 @@ exports[`support svg components add proper namespace to svg 2`] = `
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg><block-child-0/></svg>\`);
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`GComp\`, {}, key + \`__1\`, node, ctx);
@@ -1067,7 +1067,7 @@ exports[`properly behave when destroyed/unmounted while rendering 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`SubChild\`, {}, key + \`__1\`, node, ctx);
let b2 = component(\`SubChild\`, {val: ctx['props'].val}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -126,7 +126,7 @@ exports[`can catch errors can catch an error in a component render function 3`]
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -230,7 +230,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
}
return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b5 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b5]);
}
}"
@@ -250,7 +250,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -305,7 +305,7 @@ exports[`can catch errors can catch an error in the initial call of a component
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -362,7 +362,7 @@ exports[`can catch errors can catch an error in the initial call of a component
return function template(ctx, node, key = \\"\\") {
let b3;
if (ctx['state'].flag) {
b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
return block1([], [b3]);
}
@@ -417,7 +417,7 @@ exports[`can catch errors can catch an error in the mounted call 3`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -473,7 +473,7 @@ exports[`can catch errors can catch an error in the willPatch call 3`] = `
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].message;
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([d1], [b3]);
}
}"
@@ -527,7 +527,7 @@ exports[`can catch errors can catch an error in the willStart call 3`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -597,7 +597,7 @@ exports[`can catch errors can catch an error origination from a child's willStar
}
return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b5 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b5]);
}
}"
@@ -531,7 +531,7 @@ exports[`lifecycle hooks mounted hook is called on subsubcomponents, in proper o
}"
`;
exports[`lifecycle hooks onRender 1`] = `
exports[`lifecycle hooks onWillRender 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
@@ -548,14 +548,14 @@ exports[`lifecycle hooks onRender 1`] = `
}"
`;
exports[`lifecycle hooks onRender 2`] = `
exports[`lifecycle hooks onWillRender 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {}, key + \`__1\`, node, ctx);
return component(\`Child\`, {someValue: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -53,7 +53,7 @@ exports[`refs refs are properly bound in slots 2`] = `
const refs = ctx.__owl__.refs;
let d1 = ctx['state'].val;
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'footer': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'footer': slot3(ctx2)}});
return block1([d1], [b3]);
}
}"
@@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering semantics blabla 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].a.b);
}
}"
`;
exports[`rendering semantics blabla 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {a: ctx['state']}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`rendering semantics can force a render to update sub tree 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics can force a render to update sub tree 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics can render a parent without rendering child 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics can render a parent without rendering child 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
@@ -37,7 +37,7 @@ exports[`slots can define and call slots 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b4 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'header': slot3(ctx2), 'footer': slot4(ctx2)}});
let b4 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'header': slot3(ctx2), 'footer': slot4(ctx2)}});
return block1([], [b4]);
}
}"
@@ -74,7 +74,7 @@ exports[`slots can render node with t-ref and Component in same slot 2`] = `
return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs;
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -117,7 +117,7 @@ exports[`slots content is the default slot (variation) 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -152,7 +152,7 @@ exports[`slots content is the default slot 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -226,7 +226,7 @@ exports[`slots default content is not rendered if named slot is provided 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'header': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'header': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -265,7 +265,7 @@ exports[`slots default content is not rendered if slot is provided 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -310,7 +310,7 @@ exports[`slots default slot next to named slot, with default content 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'footer': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'footer': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -340,7 +340,7 @@ exports[`slots default slot work with text nodes (variation) 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -374,7 +374,7 @@ exports[`slots default slot work with text nodes 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -422,7 +422,7 @@ exports[`slots dynamic t-slot call 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b6 = assign(component(\`Toggler\`, {}, key + \`__1\`, node, ctx), {slots: {'slot1': slot3(ctx2), 'slot2': slot4(ctx2)}});
let b6 = assign(component(\`Toggler\`, {}, key + \`__1\`, node, ctx, true), {slots: {'slot1': slot3(ctx2), 'slot2': slot4(ctx2)}});
return block1([], [b6]);
}
}"
@@ -473,7 +473,7 @@ exports[`slots dynamic t-slot call with default 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b6 = assign(component(\`Toggler\`, {}, key + \`__1\`, node, ctx), {slots: {'slot1': slot3(ctx2), 'slot2': slot4(ctx2)}});
let b6 = assign(component(\`Toggler\`, {}, key + \`__1\`, node, ctx, true), {slots: {'slot1': slot3(ctx2), 'slot2': slot4(ctx2)}});
return block1([], [b6]);
}
}"
@@ -505,7 +505,7 @@ exports[`slots fun: two calls to the same slot 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -574,7 +574,7 @@ exports[`slots multiple roots are allowed in a default slot 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b5 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b5]);
}
}"
@@ -614,7 +614,7 @@ exports[`slots multiple roots are allowed in a named slot 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b5 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'content': slot3(ctx2)}});
let b5 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'content': slot3(ctx2)}});
return block1([], [b5]);
}
}"
@@ -668,7 +668,7 @@ exports[`slots multiple slots containing components 3`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
return assign(component(\`B\`, {}, key + \`__1\`, node, ctx), {slots: {'s1': slot3(ctx2), 's2': slot5(ctx2)}});
return assign(component(\`B\`, {}, key + \`__1\`, node, ctx, true), {slots: {'s1': slot3(ctx2), 's2': slot5(ctx2)}});
}
}"
`;
@@ -707,7 +707,7 @@ exports[`slots named slot inside slot 2`] = `
const slot4 = ctx => (node, key) => {
const ctx6 = capture(ctx);
return assign(component(\`Child\`, {}, key + \`__5\`, node, ctx), {slots: {'brol': slot7(ctx6)}});
return assign(component(\`Child\`, {}, key + \`__5\`, node, ctx, true), {slots: {'brol': slot7(ctx6)}});
}
const slot7 = ctx => (node, key) => {
@@ -717,7 +717,7 @@ exports[`slots named slot inside slot 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b5 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'brol': slot3(ctx2), 'default': slot4(ctx2)}});
let b5 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'brol': slot3(ctx2), 'default': slot4(ctx2)}});
return block1([], [b5]);
}
}"
@@ -757,7 +757,7 @@ exports[`slots named slot inside slot, part 3 2`] = `
const slot4 = ctx => (node, key) => {
const ctx6 = capture(ctx);
return assign(component(\`Child\`, {}, key + \`__5\`, node, ctx), {slots: {'brol': slot7(ctx6)}});
return assign(component(\`Child\`, {}, key + \`__5\`, node, ctx, true), {slots: {'brol': slot7(ctx6)}});
}
const slot7 = ctx => (node, key) => {
@@ -767,7 +767,7 @@ exports[`slots named slot inside slot, part 3 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b5 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'brol': slot3(ctx2), 'default': slot4(ctx2)}});
let b5 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'brol': slot3(ctx2), 'default': slot4(ctx2)}});
return block1([], [b5]);
}
}"
@@ -850,7 +850,7 @@ exports[`slots named slots inside slot, again 2`] = `
const slot4 = ctx => (node, key) => {
const ctx6 = capture(ctx);
return assign(component(\`Child\`, {}, key + \`__5\`, node, ctx), {slots: {'brol2': slot7(ctx6)}});
return assign(component(\`Child\`, {}, key + \`__5\`, node, ctx, true), {slots: {'brol2': slot7(ctx6)}});
}
const slot7 = ctx => (node, key) => {
@@ -860,7 +860,7 @@ exports[`slots named slots inside slot, again 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b5 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'brol1': slot3(ctx2), 'default': slot4(ctx2)}});
let b5 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'brol1': slot3(ctx2), 'default': slot4(ctx2)}});
return block1([], [b5]);
}
}"
@@ -920,7 +920,7 @@ exports[`slots nested slots in same template 4`] = `
let block1 = createBlock(\`<span id=\\"parent\\"><block-child-0/></span>\`);
const slot2 = ctx => (node, key) => {
return assign(component(\`Child2\`, {}, key + \`__3\`, node, ctx), {slots: {'default': slot4(ctx)}});
return assign(component(\`Child2\`, {}, key + \`__3\`, node, ctx, true), {slots: {'default': slot4(ctx)}});
}
const slot4 = ctx => (node, key) => {
@@ -928,7 +928,7 @@ exports[`slots nested slots in same template 4`] = `
}
return function template(ctx, node, key = \\"\\") {
let b4 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b4 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b4]);
}
}"
@@ -976,7 +976,7 @@ exports[`slots nested slots: evaluation context and parented relationship 3`] =
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`GrandChild\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`GrandChild\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -993,7 +993,7 @@ exports[`slots nested slots: evaluation context and parented relationship 4`] =
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -1052,7 +1052,7 @@ exports[`slots simple default slot 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -1081,7 +1081,7 @@ exports[`slots simple default slot, variation 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -1131,7 +1131,7 @@ exports[`slots slot and (inline) t-call 3`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1182,7 +1182,7 @@ exports[`slots slot and t-call 3`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1217,7 +1217,7 @@ exports[`slots slot and t-esc 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -1270,7 +1270,7 @@ exports[`slots slot are properly rendered if inner props are changed 3`] = `
const v1 = ctx['inc'];
let d1 = [v1, ctx];
let d2 = ctx['state'].val;
let b3 = assign(component(\`GenericComponent\`, {}, key + \`__2\`, node, ctx), {slots: {'default': slot3(ctx)}});
let b3 = assign(component(\`GenericComponent\`, {}, key + \`__2\`, node, ctx, true), {slots: {'default': slot3(ctx)}});
return block1([d1, d2], [b3]);
}
}"
@@ -1307,7 +1307,7 @@ exports[`slots slot content is bound to caller 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -1350,7 +1350,7 @@ exports[`slots slot preserves properly parented relationship 3`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -1408,7 +1408,7 @@ exports[`slots slot preserves properly parented relationship, even through t-cal
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1441,7 +1441,7 @@ exports[`slots slots and wrapper components 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Link\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Link\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -1480,7 +1480,7 @@ exports[`slots slots are rendered with proper context 2`] = `
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].val;
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'footer': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'footer': slot3(ctx2)}});
return block1([d1], [b3]);
}
}"
@@ -1525,7 +1525,7 @@ exports[`slots slots are rendered with proper context, part 2 2`] = `
ctx[\`user\`] = v_block2[i1];
let key1 = ctx['user'].id;
const ctx2 = capture(ctx);
let b7 = assign(component(\`Link\`, {to: '/user/'+ctx['user'].id}, key + \`__1__\${key1}\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b7 = assign(component(\`Link\`, {to: '/user/'+ctx['user'].id}, key + \`__1__\${key1}\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
c_block2[i1] = withKey(block3([], [b7]), key1);
}
let b2 = list(c_block2);
@@ -1574,7 +1574,7 @@ exports[`slots slots are rendered with proper context, part 3 2`] = `
let key1 = ctx['user'].id;
setContextValue(ctx, \\"userdescr\\", 'User '+ctx['user'].name);
const ctx2 = capture(ctx);
let b5 = assign(component(\`Link\`, {to: '/user/'+ctx['user'].id}, key + \`__1__\${key1}\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b5 = assign(component(\`Link\`, {to: '/user/'+ctx['user'].id}, key + \`__1__\${key1}\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
c_block2[i1] = withKey(block3([], [b5]), key1);
}
let b2 = list(c_block2);
@@ -1617,7 +1617,7 @@ exports[`slots slots are rendered with proper context, part 4 2`] = `
ctx[isBoundary] = 1
setContextValue(ctx, \\"userdescr\\", 'User '+ctx['state'].user.name);
const ctx2 = capture(ctx);
let b3 = assign(component(\`Link\`, {to: '/user/'+ctx['state'].user.id}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`Link\`, {to: '/user/'+ctx['state'].user.id}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1652,7 +1652,7 @@ exports[`slots slots in slots, with vars 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`B\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`B\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -1678,7 +1678,7 @@ exports[`slots slots in slots, with vars 3`] = `
ctx[isBoundary] = 1
setContextValue(ctx, \\"test\\", ctx['state'].name);
const ctx2 = capture(ctx);
let b3 = assign(component(\`A\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`A\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1721,7 +1721,7 @@ exports[`slots slots in t-foreach and re-rendering 2`] = `
ctx[\`n_index\`] = i1;
let key1 = ctx['n_index'];
const ctx2 = capture(ctx);
c_block2[i1] = withKey(assign(component(\`Child\`, {}, key + \`__1__\${key1}\`, node, ctx), {slots: {'default': slot3(ctx2)}}), key1);
c_block2[i1] = withKey(assign(component(\`Child\`, {}, key + \`__1__\${key1}\`, node, ctx, true), {slots: {'default': slot3(ctx2)}}), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
@@ -1775,7 +1775,7 @@ exports[`slots slots in t-foreach in t-foreach 2`] = `
ctx[\`node2\`] = v_block6[i2];
let key2 = ctx['node2'].key;
const ctx2 = capture(ctx);
c_block6[i2] = withKey(assign(component(\`Child\`, {}, key + \`__1__\${key1}__\${key2}\`, node, ctx), {slots: {'default': slot3(ctx2)}}), key2);
c_block6[i2] = withKey(assign(component(\`Child\`, {}, key + \`__1__\${key1}__\${key2}\`, node, ctx, true), {slots: {'default': slot3(ctx2)}}), key2);
}
ctx = ctx.__proto__;
let b6 = list(c_block6);
@@ -1828,7 +1828,7 @@ exports[`slots slots in t-foreach with t-set and re-rendering 2`] = `
let key1 = ctx['n_index'];
setContextValue(ctx, \\"dummy\\", ctx['n_index']);
const ctx2 = capture(ctx);
c_block2[i1] = withKey(assign(component(\`Child\`, {}, key + \`__1__\${key1}\`, node, ctx), {slots: {'default': slot3(ctx2)}}), key1);
c_block2[i1] = withKey(assign(component(\`Child\`, {}, key + \`__1__\${key1}\`, node, ctx, true), {slots: {'default': slot3(ctx2)}}), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
@@ -1867,7 +1867,7 @@ exports[`slots t-debug on a t-set-slot (defining a slot) 2`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'content': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'content': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1906,7 +1906,7 @@ exports[`slots t-set t-value in a slot 2`] = `
ctx = Object.create(ctx);
ctx[isBoundary] = 1
const ctx2 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -1968,7 +1968,7 @@ exports[`slots t-slot in recursive templates 2`] = `
ctx = Object.create(ctx);
ctx[isBoundary] = 1
const ctx2 = capture(ctx);
return assign(component(\`Wrapper\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
return assign(component(\`Wrapper\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
}
}"
`;
@@ -2027,7 +2027,7 @@ exports[`slots t-slot nested within another slot 4`] = `
let block1 = createBlock(\`<span id=\\"c2\\"><block-child-0/></span>\`);
const slot2 = ctx => (node, key) => {
return assign(component(\`Portal\`, {}, key + \`__3\`, node, ctx), {slots: {'default': slot4(ctx)}});
return assign(component(\`Portal\`, {}, key + \`__3\`, node, ctx, true), {slots: {'default': slot4(ctx)}});
}
const slot4 = ctx => (node, key) => {
@@ -2035,7 +2035,7 @@ exports[`slots t-slot nested within another slot 4`] = `
}
return function template(ctx, node, key = \\"\\") {
let b4 = assign(component(\`Modal\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b4 = assign(component(\`Modal\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b4]);
}
}"
@@ -2055,7 +2055,7 @@ exports[`slots t-slot nested within another slot 5`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -2090,7 +2090,7 @@ exports[`slots t-slot scope context 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Wrapper\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Wrapper\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -2109,7 +2109,7 @@ exports[`slots t-slot scope context 3`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -2174,7 +2174,7 @@ exports[`slots t-slot within dynamic t-call 4`] = `
return function template(ctx, node, key = \\"\\") {
const ctx2 = capture(ctx);
let b3 = assign(component(\`Slotted\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b3 = assign(component(\`Slotted\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
return block1([], [b3]);
}
}"
@@ -2221,7 +2221,7 @@ exports[`slots template can just return a slot 3`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`SlotComponent\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`SlotComponent\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -24,7 +24,7 @@ exports[`t-props basic use 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, ctx['some'].obj, key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, Object.assign({}, ctx['some'].obj), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -85,7 +85,7 @@ exports[`t-props t-props only 2`] = `
let block1 = createBlock(\`<div><div><block-child-0/></div></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Comp\`, ctx['state'], key + \`__1\`, node, ctx);
let b2 = component(\`Comp\`, Object.assign({}, ctx['state']), key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
@@ -38,7 +38,7 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 2`] = `
setContextValue(ctx, \\"iter\\", 'source');
let d1 = ctx['iter'];
const ctx2 = capture(ctx);
let b2 = assign(component(\`Childcomp\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}});
let b2 = assign(component(\`Childcomp\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot3(ctx2)}});
let d2 = ctx['iter'];
return block1([d1, d2], [b2]);
}
+17 -17
View File
@@ -40,14 +40,14 @@ describe("basics", () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
let error;
let error: Error;
try {
await mount(SomeWidget, document.createDocumentFragment() as any);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot mount component: the target is not a valid DOM element");
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount component: the target is not a valid DOM element");
});
test("can mount a simple component with props", async () => {
@@ -160,28 +160,28 @@ describe("basics", () => {
static template = xml`<span>simple vnode</span>`;
}
let error;
let error: Error;
try {
await mount(Test, null as any);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot mount component: the target is not a valid DOM element");
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount component: the target is not a valid DOM element");
});
test("a component cannot be mounted in a detached node", async () => {
class Test extends Component {
static template = xml`<div/>`;
}
let error;
let error: Error;
try {
await mount(Test, document.createElement("div"));
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot mount a component on a detached dom node");
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
});
test("crashes if it cannot find a template", async () => {
@@ -189,14 +189,14 @@ describe("basics", () => {
static template = "wrongtemplate";
}
let error;
let error: Error;
try {
await mount(Test, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe('Missing template: "wrongtemplate"');
expect(error!).toBeDefined();
expect(error!.message).toBe('Missing template: "wrongtemplate"');
});
test("class component with dynamic text", async () => {
@@ -419,7 +419,7 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("<div><span></span></div>");
});
test("child can be updated", async () => {
test.only("child can be updated", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.value"/>`;
}
+348 -173
View File
@@ -112,16 +112,20 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect(steps).toEqual([
"W:setup",
"W:willStart",
"W:render",
"W:willRender",
"W:rendered",
"W:mounted",
"W:render",
"W:willRender",
"Child:setup",
"Child:willStart",
"W:render",
"W:rendered",
"W:willRender",
"Child:destroyed",
"Child:setup",
"Child:willStart",
"Child:render",
"W:rendered",
"Child:willRender",
"Child:rendered",
"W:willPatch",
"Child:mounted",
"W:patched",
@@ -189,19 +193,24 @@ test("creating two async components, scenario 1", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"Parent:render",
"Parent:rendered",
"Parent:willRender",
"ChildA:destroyed",
"ChildA:setup",
"ChildA:willStart",
"ChildB:setup",
"ChildB:willStart",
"ChildB:render",
"ChildA:render",
"Parent:rendered",
"ChildB:willRender",
"ChildB:rendered",
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildB:mounted",
"ChildA:mounted",
@@ -263,20 +272,26 @@ test("creating two async components, scenario 2", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildA:render",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"ChildA:willUpdateProps",
"Parent:render",
"Parent:rendered",
"Parent:willRender",
"ChildA:willUpdateProps",
"ChildB:setup",
"ChildB:willStart",
"ChildB:render",
"ChildA:render",
"Parent:rendered",
"ChildB:willRender",
"ChildB:rendered",
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildB:mounted",
@@ -336,20 +351,26 @@ test("creating two async components, scenario 3 (patching in the same frame)", a
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildA:render",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"ChildA:willUpdateProps",
"Parent:render",
"Parent:rendered",
"Parent:willRender",
"ChildA:willUpdateProps",
"ChildB:setup",
"ChildB:willStart",
"ChildB:render",
"ChildA:render",
"Parent:rendered",
"ChildB:willRender",
"ChildB:rendered",
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildB:mounted",
@@ -396,17 +417,22 @@ test("update a sub-component twice in the same frame", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildA:render",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"ChildA:willUpdateProps",
"Parent:render",
"Parent:rendered",
"Parent:willRender",
"ChildA:willUpdateProps",
"ChildA:render",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildA:patched",
@@ -447,15 +473,18 @@ test("update a sub-component twice in the same frame, 2", async () => {
expect(steps.splice(0)).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildA:render",
"Parent:rendered",
"ChildA:willRender",
"render",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"ChildA:willUpdateProps",
"Parent:rendered",
]);
await nextMicroTick();
// For an unknown reason, this test fails on windows without the next microtick. It works
@@ -463,22 +492,27 @@ test("update a sub-component twice in the same frame, 2", async () => {
// I do not see anything harmful in waiting an extra tick. But it is annoying to not
// know what is different.
await nextMicroTick();
expect(steps.splice(0)).toEqual(["ChildA:render", "render"]);
expect(steps.splice(0)).toEqual(["ChildA:willRender", "render", "ChildA:rendered"]);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.valA = 3;
await nextMicroTick();
await nextMicroTick();
expect(steps.splice(0)).toEqual(["Parent:render", "ChildA:willUpdateProps"]);
expect(steps.splice(0)).toEqual([
"Parent:willRender",
"ChildA:willUpdateProps",
"Parent:rendered",
]);
await nextMicroTick();
// same as above
await nextMicroTick();
expect(steps).toEqual(["ChildA:render", "render"]);
expect(steps).toEqual(["ChildA:willRender", "render", "ChildA:rendered"]);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(steps).toEqual([
"ChildA:render",
"ChildA:willRender",
"render",
"ChildA:rendered",
"Parent:willPatch",
"ChildA:willPatch",
"ChildA:patched",
@@ -509,7 +543,7 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
}
class Child extends Component {
static template = xml`<div><SubChild /></div>`;
static template = xml`<div><SubChild val="props.val"/></div>`;
static components = { SubChild };
setup() {
useLogLifecycle(steps);
@@ -548,21 +582,27 @@ test("properly behave when destroyed/unmounted while rendering ", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"SubChild:setup",
"SubChild:willStart",
"SubChild:render",
"Child:rendered",
"SubChild:willRender",
"SubChild:rendered",
"SubChild:mounted",
"Child:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Child:willUpdateProps",
"Child:render",
"Parent:rendered",
"Child:willRender",
"SubChild:willUpdateProps",
"Parent:render",
"Child:rendered",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"SubChild:willUnmount",
@@ -612,16 +652,20 @@ test("rendering component again in next microtick", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:render",
"Parent:rendered",
"Parent:willRender",
"Child:destroyed",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
@@ -689,23 +733,30 @@ test("concurrent renderings scenario 1", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentC:mounted",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentB:render",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentA:render",
"ComponentB:rendered",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
@@ -774,23 +825,30 @@ test("concurrent renderings scenario 2", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentC:mounted",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:render",
"ComponentB:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
@@ -858,23 +916,30 @@ test("concurrent renderings scenario 2bis", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentC:mounted",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentB:render",
"ComponentB:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
@@ -964,29 +1029,38 @@ test("concurrent renderings scenario 3", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:setup",
"ComponentD:willStart",
"ComponentD:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentD:mounted",
"ComponentC:mounted",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentC:render",
"ComponentA:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentB:render",
"ComponentC:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentD:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
@@ -1078,29 +1152,38 @@ test("concurrent renderings scenario 4", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:setup",
"ComponentD:willStart",
"ComponentD:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentD:mounted",
"ComponentC:mounted",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentC:render",
"ComponentA:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentB:render",
"ComponentC:rendered",
"ComponentB:willRender",
"ComponentC:willUpdateProps",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentD:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
@@ -1164,17 +1247,22 @@ test("concurrent renderings scenario 5", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:render",
"ComponentA:rendered",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
@@ -1235,17 +1323,22 @@ test("concurrent renderings scenario 6", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentA:render",
"ComponentA:rendered",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
@@ -1294,15 +1387,19 @@ test("concurrent renderings scenario 7", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
@@ -1354,15 +1451,19 @@ test("concurrent renderings scenario 8", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentB:patched",
@@ -1446,30 +1547,40 @@ test("concurrent renderings scenario 9", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentB:render",
"ComponentC:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentD:setup",
"ComponentD:willStart",
"ComponentD:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentD:mounted",
"ComponentC:mounted",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentC:willUpdateProps",
"ComponentC:render",
"ComponentA:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentD:render",
"ComponentC:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentC:willRender",
"ComponentD:willUpdateProps",
"ComponentD:render",
"ComponentB:render",
"ComponentC:rendered",
"ComponentD:willRender",
"ComponentD:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:willPatch",
@@ -1554,22 +1665,28 @@ test("concurrent renderings scenario 10", async () => {
expect(steps).toEqual([
"ComponentA:setup",
"ComponentA:willStart",
"ComponentA:render",
"ComponentA:willRender",
"ComponentB:setup",
"ComponentB:willStart",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentB:rendered",
"ComponentB:mounted",
"ComponentA:mounted",
"ComponentB:render",
"ComponentB:willRender",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentA:render",
"ComponentB:rendered",
"ComponentA:willRender",
"ComponentB:willUpdateProps",
"ComponentB:render",
"ComponentA:rendered",
"ComponentB:willRender",
"ComponentC:destroyed",
"ComponentC:setup",
"ComponentC:willStart",
"ComponentC:render",
"ComponentB:rendered",
"ComponentC:willRender",
"ComponentC:rendered",
"ComponentA:willPatch",
"ComponentB:willPatch",
"ComponentC:mounted",
@@ -1624,15 +1741,19 @@ test("concurrent renderings scenario 11", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Child:willUpdateProps",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
@@ -1697,17 +1818,22 @@ test("concurrent renderings scenario 12", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Child:willUpdateProps",
"Parent:render",
"Parent:rendered",
"Parent:willRender",
"Child:willUpdateProps",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
@@ -1761,28 +1887,31 @@ test("concurrent renderings scenario 13", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Child:render",
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
"Parent:render",
"Child:willUpdateProps",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:mounted",
"Child:patched",
"Parent:patched",
"Child:render",
"Child:render",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
"Child:willPatch",
@@ -1856,24 +1985,32 @@ test("concurrent renderings scenario 14", async () => {
expect(steps).toEqual([
"A:setup",
"A:willStart",
"A:render",
"A:willRender",
"B:setup",
"B:willStart",
"B:render",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"C:render",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
"A:render",
"A:willRender",
"B:willUpdateProps",
"B:render",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"C:render",
"B:render",
"B:rendered",
"C:willRender",
"C:rendered",
"B:willRender",
"C:willUpdateProps",
"C:render",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
@@ -1955,24 +2092,32 @@ test("concurrent renderings scenario 15", async () => {
expect(steps).toEqual([
"A:setup",
"A:willStart",
"A:render",
"A:willRender",
"B:setup",
"B:willStart",
"B:render",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"C:render",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
"A:render",
"A:willRender",
"B:willUpdateProps",
"B:render",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"C:render",
"B:render",
"B:rendered",
"C:willRender",
"C:rendered",
"B:willRender",
"C:willUpdateProps",
"C:render",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
@@ -2063,30 +2208,39 @@ test.skip("concurrent renderings scenario 16", async () => {
expect(steps).toEqual([
"A:setup",
"A:willStart",
"A:render",
"A:willRender",
"B:setup",
"B:willStart",
"B:render",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"C:render",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
"A:render",
"A:willRender",
"B:willUpdateProps",
"B:render",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willUpdateProps",
"C:render",
"C:willRender",
"C:rendered",
"D:setup",
"D:willStart",
"B:render",
"B:willRender",
"B:rendered",
"C:willUpdateProps",
"C:render",
"C:willRender",
"C:rendered",
"D:destroyed",
"D:setup",
"D:willStart",
"D:render",
"D:willRender",
"D:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
@@ -2209,9 +2363,11 @@ test("change state and call manually render: no unnecessary rendering", async ()
expect(steps).toEqual([
"Test:setup",
"Test:willStart",
"Test:render",
"Test:willRender",
"Test:rendered",
"Test:mounted",
"Test:render",
"Test:willRender",
"Test:rendered",
"Test:willPatch",
"Test:patched",
]);
@@ -2242,7 +2398,13 @@ test("changing state before first render does not trigger a render", async () =>
await nextTick();
expect(renders).toBe(1);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(steps).toEqual(["TestW:setup", "TestW:willStart", "TestW:render", "TestW:mounted"]);
expect(steps).toEqual([
"TestW:setup",
"TestW:willStart",
"TestW:willRender",
"TestW:rendered",
"TestW:mounted",
]);
Object.freeze(steps);
});
@@ -2278,17 +2440,25 @@ test("changing state before first render does not trigger a render (with parent)
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]);
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0);
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>3</div></div>");
expect(renders).toBe(1);
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"TestW:setup",
"TestW:willStart",
"TestW:render",
"Parent:rendered",
"TestW:willRender",
"TestW:rendered",
"Parent:willPatch",
"TestW:mounted",
"Parent:patched",
@@ -2342,10 +2512,12 @@ test("two renderings initiated between willPatch and patched", async () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Panel:setup",
"Panel:willStart",
"Panel:render",
"Parent:rendered",
"Panel:willRender",
"Panel:rendered",
"Panel:mounted",
"Parent:mounted",
]);
@@ -2356,10 +2528,12 @@ test("two renderings initiated between willPatch and patched", async () => {
expect(fixture.innerHTML).toBe("<div><abc>Panel2</abc></div>");
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"Panel:setup",
"Panel:willStart",
"Panel:render",
"Parent:rendered",
"Panel:willRender",
"Panel:rendered",
"Parent:willPatch",
"Panel:willUnmount",
"Panel:destroyed",
@@ -2373,7 +2547,8 @@ test("two renderings initiated between willPatch and patched", async () => {
expect(fixture.innerHTML).toBe("<div></div>");
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Panel:willUnmount",
"Panel:destroyed",
+38 -38
View File
@@ -35,18 +35,18 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
parent.state.flag = true;
let error;
let error: Error;
try {
await parent.render();
} catch (e) {
error = e;
error = e as Error;
}
expect(fixture.innerHTML).toBe("");
expect(status(parent)).toBe("destroyed");
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
});
test("display a nice error if it cannot find component", async () => {
@@ -58,14 +58,14 @@ describe("basics", () => {
static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent };
}
let error;
let error: Error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(error!).toBeDefined();
expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
@@ -108,16 +108,16 @@ describe("errors and promises", () => {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
let error;
let error: Error;
try {
await mount(App, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -136,14 +136,14 @@ describe("errors and promises", () => {
}
}
let error;
let error: Error;
try {
await mount(App, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(error!).toBeDefined();
expect(error!.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
@@ -166,14 +166,14 @@ describe("errors and promises", () => {
const app = await mount(App, fixture);
app.val = 4;
let error;
let error: Error;
try {
await app.render();
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(error!).toBeDefined();
expect(error!.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
@@ -196,14 +196,14 @@ describe("errors and promises", () => {
const app = await mount(App, fixture);
app.val = 4;
let error;
let error: Error;
try {
await app.render();
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(error!).toBeDefined();
expect(error!.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
@@ -222,16 +222,16 @@ describe("errors and promises", () => {
static components = { Child };
}
let error;
let error: Error;
try {
await mount(App, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -249,16 +249,16 @@ describe("errors and promises", () => {
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
app.flag = true;
let error;
let error: Error;
try {
await app.render();
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -273,20 +273,20 @@ describe("errors and promises", () => {
static components = { Child };
}
let error;
let error: Error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
});
test("errors in mounted and in willUnmount", async () => {
expect.assertions(2); // apparently this expect count in assertions...
expect.assertions(2);
class Example extends Component {
static template = xml`<div/>`;
val: any;
@@ -305,7 +305,7 @@ describe("errors and promises", () => {
try {
await mount(Example, fixture);
} catch (e) {
expect(e.message).toBe("Error in mounted");
expect((e as Error).message).toBe("Error in mounted");
}
});
});
+98 -47
View File
@@ -4,7 +4,7 @@ import {
onWillUnmount,
onPatched,
onWillUpdateProps,
onRender,
onWillRender,
} from "../../src/component/lifecycle_hooks";
import { status } from "../../src/component/status";
import { xml } from "../../src/tags";
@@ -385,20 +385,25 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Child:willUpdateProps",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
"Parent:render",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:destroyed",
@@ -561,10 +566,12 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]);
@@ -609,20 +616,29 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]);
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0);
parent.state.hasChild = true;
await nextTick();
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:render",
"Child:rendered",
"GrandChild:willRender",
"GrandChild:rendered",
"Parent:willPatch",
"GrandChild:mounted",
"Child:mounted",
@@ -672,7 +688,13 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]);
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0);
@@ -717,7 +739,13 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]);
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0);
@@ -725,12 +753,14 @@ describe("lifecycle hooks", () => {
await nextTick();
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"Child:rendered",
]);
steps.splice(0);
@@ -769,10 +799,12 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]);
@@ -783,7 +815,8 @@ describe("lifecycle hooks", () => {
await nextTick();
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:destroyed",
@@ -816,10 +849,12 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]);
@@ -830,9 +865,11 @@ describe("lifecycle hooks", () => {
await nextTick();
expect(steps).toEqual([
"Parent:render",
"Parent:willRender",
"Child:willUpdateProps",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
@@ -841,7 +878,7 @@ describe("lifecycle hooks", () => {
Object.freeze(steps);
});
test("onRender", async () => {
test("onWillRender", async () => {
let steps: string[] = [];
const def = makeDeferred();
@@ -852,7 +889,7 @@ describe("lifecycle hooks", () => {
setup() {
useLogLifecycle(steps);
onWillUpdateProps(() => def);
onRender(() => (this.visibleState = this.state.value));
onWillRender(() => (this.visibleState = this.state.value));
}
increment() {
this.state.value++;
@@ -862,8 +899,9 @@ describe("lifecycle hooks", () => {
class Parent extends Component {
static template = xml`
<Child />`;
<Child someValue="state.value" />`;
static components = { Child };
state = useState({ value: 1 });
setup() {
useLogLifecycle(steps);
}
@@ -873,7 +911,7 @@ describe("lifecycle hooks", () => {
expect(fixture.innerHTML).toBe("<button>1</button>");
parent.render(); // to block child render
parent.state.value++; // to block child render
await nextTick();
fixture.querySelector("button")!.click();
@@ -891,17 +929,21 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"inc:1",
"inc:1",
"Child:render",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
@@ -993,19 +1035,24 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"A:setup",
"A:willStart",
"A:render",
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"B:render",
"C:render",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"D:setup",
"D:willStart",
"E:setup",
"E:willStart",
"D:render",
"E:render",
"C:rendered",
"D:willRender",
"D:rendered",
"E:willRender",
"E:rendered",
"E:mounted",
"D:mounted",
"C:mounted",
@@ -1017,19 +1064,18 @@ describe("lifecycle hooks", () => {
steps.splice(0);
c!.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe(`<div>A<div>B</div><div>C<div>D</div><div>F</div></div></div>`);
expect(steps).toEqual([
"C:render",
"D:willUpdateProps",
"C:willRender",
"F:setup",
"F:willStart",
"D:render",
"F:render",
"C:rendered",
"F:willRender",
"F:rendered",
"C:willPatch",
"D:willPatch",
"E:willUnmount",
"E:destroyed",
"F:mounted",
"D:patched",
"C:patched",
]);
});
@@ -1062,21 +1108,26 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:destroyed",
"Parent:patched",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
+116 -116
View File
@@ -46,23 +46,23 @@ describe("props validation", () => {
static components = { SubComp };
static template = xml`<div><SubComp /></div>`;
}
let error;
let error: Error | undefined;
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'SubComp')`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Missing props 'message' (component 'SubComp')`);
error = undefined;
try {
await mountApp(Parent, false);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
});
test("props: list of strings", async () => {
@@ -75,14 +75,14 @@ describe("props validation", () => {
static template = xml`<div><SubComp /></div>`;
}
let error;
let error: Error;
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'SubComp')`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Missing props 'message' (component 'SubComp')`);
});
test("validate simple types", async () => {
@@ -108,32 +108,32 @@ describe("props validation", () => {
};
(Parent as any).components = { SubComp };
let error;
let error: Error | undefined;
props = {};
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Missing props 'p' (component '_a')`);
error = undefined;
props = { p: test.ok };
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
props = { p: test.ko };
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component '_a'");
}
});
@@ -159,31 +159,31 @@ describe("props validation", () => {
static template = xml`<div>hey</div>`;
};
(Parent as any).components = { SubComp };
let error;
let error: Error | undefined;
props = {};
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Missing props 'p' (component '_a')`);
error = undefined;
props = { p: test.ok };
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
props = { p: test.ko };
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component '_a'");
}
});
@@ -199,30 +199,30 @@ describe("props validation", () => {
return props.p;
}
}
let error;
let error: Error;
let props: { p?: any };
try {
props = { p: "string" };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: true };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: 1 };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
});
test("can validate an optional props", async () => {
@@ -237,30 +237,30 @@ describe("props validation", () => {
return props.p;
}
}
let error;
let error: Error;
let props: { p?: any };
try {
props = { p: "key" };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = {};
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: 1 };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
});
test("can validate an array with given primitive type", async () => {
@@ -275,35 +275,35 @@ describe("props validation", () => {
return props.p;
}
}
let error;
let error: Error | undefined;
let props: { p?: any };
try {
props = { p: [] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: ["string"] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: [1] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
error = undefined;
try {
props = { p: ["string", 1] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
});
@@ -319,37 +319,37 @@ describe("props validation", () => {
return props.p;
}
}
let error;
let error: Error;
let props: { p?: any };
try {
props = { p: [] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: ["string"] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: [false, true, "string"] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: [true, 1] };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
});
test("can validate an object with simple shape", async () => {
@@ -366,40 +366,40 @@ describe("props validation", () => {
return props.p;
}
}
let error;
let error: Error | undefined;
let props: { p?: any };
try {
props = { p: { id: 1, url: "url" } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: "url", extra: true } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid prop 'p' in component SubComp (unknown prop 'extra')");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid prop 'p' in component SubComp (unknown prop 'extra')");
try {
props = { p: { id: "1", url: "url" } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
error = undefined;
try {
props = { p: { id: 1 } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
});
test("can validate recursively complicated prop def", async () => {
@@ -422,30 +422,30 @@ describe("props validation", () => {
return props.p;
}
}
let error;
let error: Error;
let props: { p?: any };
try {
props = { p: { id: 1, url: true } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: [12] } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: [12, true] } };
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
});
test("can validate optional attributes in nested sub props", () => {
@@ -462,20 +462,20 @@ describe("props validation", () => {
},
};
}
let error;
let error: Error;
try {
validateProps(TestComponent as any, { myprop: [{}] });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
validateProps(TestComponent as any, { myprop: [{ a: 1 }] });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
);
});
@@ -488,20 +488,20 @@ describe("props validation", () => {
},
};
}
let error;
let error: Error;
try {
validateProps(TestComponent as any, { size: "small" });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
try {
validateProps(TestComponent as any, { size: "abcdef" });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
});
test("can validate with a custom validator, and a type", () => {
@@ -514,30 +514,30 @@ describe("props validation", () => {
},
};
}
let error;
let error: Error | undefined;
try {
validateProps(TestComponent as any, { n: 3 });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeUndefined();
expect(error!).toBeUndefined();
expect(validator).toBeCalledTimes(1);
try {
validateProps(TestComponent as any, { n: "str" });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(validator).toBeCalledTimes(1);
error = null;
error = undefined;
try {
validateProps(TestComponent as any, { n: 100 });
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(validator).toBeCalledTimes(2);
});
@@ -647,18 +647,18 @@ describe("props validation", () => {
static template = xml`<div><SubComp/></div>`;
static components = { SubComp };
}
let error;
let error: Error;
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'SubComp')");
expect(error!).toBeDefined();
expect(error!.message).toBe("Missing props 'p' (component 'SubComp')");
});
test("props are validated whenever component is updated", async () => {
let error;
let error: Error;
class SubComp extends Component {
static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`;
@@ -674,10 +674,10 @@ describe("props validation", () => {
(app as any).root.component.state.p = undefined;
await (app as any).root.component.render();
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'SubComp')");
expect(error!).toBeDefined();
expect(error!.message).toBe("Missing props 'p' (component 'SubComp')");
});
test("default values are applied before validating props at update", async () => {
@@ -712,14 +712,14 @@ describe("props validation", () => {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
let error;
let error: Error;
try {
await mountApp(Parent);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("Missing props 'mandatory' (component 'Child')");
expect(error!).toBeDefined();
expect(error!.message).toBe("Missing props 'mandatory' (component 'Child')");
});
});
+4 -4
View File
@@ -2,7 +2,7 @@ import {
Component,
mount,
onPatched,
onRender,
onWillRender,
onWillPatch,
onWillUnmount,
useState,
@@ -37,7 +37,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`<div><t t-esc="state.a"/></div>`;
state = useState({ a: 5, b: 7 });
setup() {
onRender(() => n++);
onWillRender(() => n++);
}
}
const comp = await mount(Comp, fixture);
@@ -61,7 +61,7 @@ describe("reactivity in lifecycle", () => {
`;
state = useState({ n: 2 });
setup() {
onRender(() => {
onWillRender(() => {
steps.push("render");
});
onWillPatch(() => {
@@ -132,7 +132,7 @@ describe("reactivity in lifecycle", () => {
state = useState({ val: 1 });
setup() {
STATE = this.state;
onRender(() => {
onWillRender(() => {
steps.push(this.state.val);
});
}
+110
View File
@@ -0,0 +1,110 @@
import { Component, mount, onRendered, useState } from "../../src";
import { xml } from "../../src/tags";
import { makeTestFixture, snapshotEverything, nextTick } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("rendering semantics", () => {
test("can render a parent without rendering child", async () => {
let childN = 0;
let parentN = 0;
class Child extends Component {
static template = xml`child`;
setup() {
onRendered(() => childN++);
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<Child/>
`;
static components = { Child };
state = useState({ value: "A" });
setup() {
onRendered(() => parentN++);
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect(parentN).toBe(1);
expect(childN).toBe(1);
parent.state.value = "B";
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect(parentN).toBe(2);
expect(childN).toBe(1);
});
test("can force a render to update sub tree", async () => {
let childN = 0;
let parentN = 0;
class Child extends Component {
static template = xml`child`;
setup() {
onRendered(() => childN++);
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<Child/>
`;
static components = { Child };
state = { value: "A" };
setup() {
onRendered(() => parentN++);
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect(parentN).toBe(1);
expect(childN).toBe(1);
parent.state.value = "B";
parent.render(true);
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect(parentN).toBe(2);
expect(childN).toBe(2);
});
test("blabla", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.a.b"/>`;
}
class Parent extends Component {
static template = xml`
<Child a="state"/>
`;
static components = { Child };
state = useState({ b: 1 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
parent.state.b = 3;
await nextTick();
expect(fixture.innerHTML).toBe("3");
});
});
+4 -4
View File
@@ -338,16 +338,16 @@ describe("style and class handling", () => {
static template = xml`<Child class="'a'"/>`;
static components = { Child };
}
let error;
let error: Error;
try {
await mount(ParentWidget, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
});
});
+4 -4
View File
@@ -87,14 +87,14 @@ describe("styles and component", () => {
static template = xml`<div class="app">text</div>`;
static style = `.app {color: red;}`;
}
let error;
let error: Error;
try {
await mount(App, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
);
});
+16 -10
View File
@@ -35,10 +35,12 @@ describe("t-component", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]);
@@ -80,16 +82,20 @@ describe("t-component", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildA:render",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
"Parent:render",
"Parent:willRender",
"ChildB:setup",
"ChildB:willStart",
"ChildB:render",
"Parent:rendered",
"ChildB:willRender",
"ChildB:rendered",
"Parent:willPatch",
"ChildA:willUnmount",
"ChildA:destroyed",
@@ -199,14 +205,14 @@ describe("t-component", () => {
static components = { Child };
static template = xml`<div><div t-component="Child"/></div>`;
}
let error;
let error: Error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(
expect(error!).toBeDefined();
expect(error!.message).toBe(
`Directive 't-component' can only be used on <t> nodes (used on a <div>)`
);
});
+6 -3
View File
@@ -81,13 +81,16 @@ describe("list of components", () => {
expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:render",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:setup",
"Child:willStart",
"Child:render",
"Child:render",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
+4 -4
View File
@@ -57,14 +57,14 @@ describe("t-model directive", () => {
</div>`;
state = useState({ text: "" });
}
let error;
let error: Error;
try {
await mount(SomeComponent, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
});
test("basic use, on another key in component", async () => {
+2 -2
View File
@@ -42,7 +42,7 @@ describe("t-props", () => {
a: "first",
b: "second",
});
a = "third";
a? = "third";
}
const parent = await mount(Parent, fixture);
expect(fixture.textContent).toBe("thirdsecond");
@@ -65,7 +65,7 @@ describe("t-props", () => {
`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2 });
expect(this.props).toBe(props);
expect(this.props).not.toBe(props);
}
}
class Parent extends Component {
+8 -3
View File
@@ -4,13 +4,14 @@ import {
onDestroyed,
onMounted,
onPatched,
onRender,
onWillRender,
onWillPatch,
onWillStart,
onWillUnmount,
onWillUpdateProps,
status,
useComponent,
onRendered,
} from "../src";
import { BDom } from "../src/blockdom";
import { blockDom } from "../src";
@@ -181,8 +182,12 @@ export function useLogLifecycle(steps: string[]) {
steps.push(`${name}:willUpdateProps`);
});
onRender(() => {
steps.push(`${name}:render`);
onWillRender(() => {
steps.push(`${name}:willRender`);
});
onRendered(() => {
steps.push(`${name}:rendered`);
});
onWillPatch(() => {
+3 -3
View File
@@ -30,7 +30,7 @@ exports[`Memo if no prop change, prevent renderings from above 2`] = `
let b2 = text(ctx['state'].a);
let b3 = text(ctx['state'].b);
let b4 = text(ctx['state'].c);
let b9 = assign(component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b9 = assign(component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return multi([b2, b3, b4, b9]);
}
}"
@@ -61,7 +61,7 @@ exports[`Memo if no props, prevent renderings from above 2`] = `
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
let b4 = assign(component(\`Memo\`, {}, key + \`__2\`, node, ctx), {slots: {'default': slot3(ctx)}});
let b4 = assign(component(\`Memo\`, {}, key + \`__2\`, node, ctx, true), {slots: {'default': slot3(ctx)}});
return multi([b2, b4]);
}
}"
@@ -80,7 +80,7 @@ exports[`Memo if no props, prevent renderings from above (work with simple html)
return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value);
let b4 = assign(component(\`Memo\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b4 = assign(component(\`Memo\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return multi([b2, b4]);
}
}"
+20 -20
View File
@@ -28,7 +28,7 @@ exports[`Portal Portal composed with t-slot 2`] = `
}
return function template(ctx, node, key = \\"\\") {
return assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
return assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
}"
`;
@@ -47,7 +47,7 @@ exports[`Portal Portal composed with t-slot 3`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -68,7 +68,7 @@ exports[`Portal basic use of portal 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -106,7 +106,7 @@ exports[`Portal conditional use of Portal (with sub Component) 2`] = `
let b2,b4;
b2 = block2();
if (ctx['state'].hasPortal) {
b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
return multi([b2, b4]);
}
@@ -131,7 +131,7 @@ exports[`Portal conditional use of Portal 1`] = `
let b2,b4;
b2 = block2();
if (ctx['state'].hasPortal) {
b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
return multi([b2, b4]);
}
@@ -169,7 +169,7 @@ exports[`Portal lifecycle hooks of portal sub component are properly called 2`]
return function template(ctx, node, key = \\"\\") {
let b3;
if (ctx['state'].hasChild) {
b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
}
return block1([], [b3]);
}
@@ -196,7 +196,7 @@ exports[`Portal portal could have dynamically no content 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b4]);
}
}"
@@ -231,7 +231,7 @@ exports[`Portal portal destroys on crash 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -266,7 +266,7 @@ exports[`Portal portal with child and props 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -295,7 +295,7 @@ exports[`Portal portal with dynamic body 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b5 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b5]);
}
}"
@@ -319,7 +319,7 @@ exports[`Portal portal with many children 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b5 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b5]);
}
}"
@@ -343,7 +343,7 @@ exports[`Portal portal with no content 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b4]);
}
}"
@@ -363,7 +363,7 @@ exports[`Portal portal with only text as content 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -384,7 +384,7 @@ exports[`Portal portal with target not in dom 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#does-not-exist'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#does-not-exist'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -418,7 +418,7 @@ exports[`Portal portal's parent's env is not polluted 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -439,7 +439,7 @@ exports[`Portal with target in template (after portal) 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#local-target'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#local-target'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -460,7 +460,7 @@ exports[`Portal with target in template (before portal) 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#local-target'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#local-target'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -481,7 +481,7 @@ exports[`Portal: Props validation target is mandatory 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -502,7 +502,7 @@ exports[`Portal: Props validation target is not list 1`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: ['body']}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: ['body']}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
@@ -537,7 +537,7 @@ exports[`Portal: UI/UX focus is kept across re-renders 2`] = `
}
return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx, true), {slots: {'default': slot2(ctx)}});
return block1([], [b3]);
}
}"
+16 -16
View File
@@ -160,15 +160,15 @@ describe("Portal", () => {
</div>`;
}
let error;
let error: Error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe("invalid portal target");
expect(error!).toBeDefined();
expect(error!.message).toBe("invalid portal target");
expect(fixture.innerHTML).toBe(`<div></div>`);
});
@@ -396,16 +396,16 @@ describe("Portal", () => {
const parent = await mount(Parent, fixture);
parent.state.error = true;
let error;
let error: Error;
try {
await parent.render();
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error!).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(error!.message).toMatch(regexp);
});
test("portal's parent's env is not polluted", async () => {
@@ -519,16 +519,16 @@ describe("Portal: Props validation", () => {
</Portal>
</div>`;
}
let error;
let error: Error;
let app = new App(Parent);
app.configure({ dev: true });
try {
await app.mount(fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Missing props 'target' (component 'Portal')`);
console.info = consoleInfo;
});
@@ -544,16 +544,16 @@ describe("Portal: Props validation", () => {
</Portal>
</div>`;
}
let error;
let error: Error;
let app = new App(Parent);
app.configure({ dev: true });
try {
await app.mount(fixture);
} catch (e) {
error = e;
error = e as Error;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
expect(error!).toBeDefined();
expect(error!.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
console.info = consoleInfo;
});
});
+498 -460
View File
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -1,4 +1,5 @@
import { EventBus } from "../src/utils";
import { batched, EventBus } from "../src/utils";
import { nextMicroTick } from "./helpers";
describe("event bus behaviour", () => {
test("can subscribe and be notified", () => {
@@ -33,3 +34,20 @@ describe("event bus behaviour", () => {
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);
});
});
+76 -169
View File
@@ -1,19 +1,11 @@
import { SAMPLES } from "./samples.js";
const { mount, hooks } = owl;
const { useState, useRef, onMounted, onWillUnmount } = hooks;
import { debounce, loadJS } from "./utils.js";
import { exportStandaloneApp } from "./export_snippet.js";
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
//------------------------------------------------------------------------------
// Constants, helpers, utils
//------------------------------------------------------------------------------
let owlJS;
async function owlSourceCode() {
if (owlJS) {
return owlJS;
}
const result = await fetch("../owl.js");
owlJS = await result.text();
return owlJS;
}
const MODES = {
js: "ace/mode/javascript",
@@ -24,77 +16,41 @@ const MODES = {
const DEFAULT_XML = `<templates>
</templates>`;
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script src="app.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server():
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
const utilsMod = doc.createElement("script");
utilsMod.setAttribute("type", "module");
utilsMod.setAttribute("src", "utils.js");
doc.head.appendChild(utilsMod);
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "text/javascript";
script.type = "module";
const content = `
{
owl.config.mode = 'dev';
let templates = \`${sanitizedXML}\`;
const qweb = new owl.QWeb({ templates });
owl.Component.env = { qweb };
}
${js}`;
import * as utils from "./utils.js";
(function (owl) {
const _configure = owl.App.prototype.configure;
owl.App.prototype.configure = function configureOverriden(config) {
config = Object.assign({ dev: true }, config);
this.addTemplates(\`${sanitizedXML}\`);
return _configure.call(this, config);
}
})(owl);
(async function() {
${js}
})()`;
script.innerHTML = content;
doc.body.appendChild(script);
});
@@ -108,58 +64,6 @@ function makeCodeIframe(js, css, xml) {
return iframe;
}
/**
* Make a zip file containing a functioning application
*/
async function makeApp(js, css, xml) {
await owl.utils.loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
async function start() {
let templates;
try {
templates = await owl.utils.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
const env = { qweb: new owl.QWeb({templates})};
owl.Component.env = env;
await owl.utils.whenReady();
app();
}
start();
`;
zip.file("app.js", JS);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSourceCode());
return zip.generateAsync({ type: "blob" });
}
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
@@ -189,7 +93,7 @@ function deleteLocalSample() {
function useSamples() {
const samples = loadSamples();
const component = owl.Component.current;
const component = owl.useComponent();
let interval;
onMounted(() => {
@@ -205,57 +109,57 @@ function useSamples() {
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
constructor(parent, props) {
super(parent, props);
setup() {
const props = this.props;
this.state = useState({
currentTab: props.js !== false ? "js" : props.xml ? "xml" : "css"
});
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.setTab = debounce(this.setTab.bind(this), 250, true);
this.sessions = {};
this._setupSessions(props);
this.editorNode = useRef("editor");
this._updateCode = this._updateCode.bind(this);
}
mounted() {
this.editor = this.editor || ace.edit(this.editorNode.el);
onMounted(() => {
this.editor = this.editor || ace.edit(this.editorNode.el);
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai");
this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
}
this.editor.setValue(this.props[this.state.currentTab], -1);
this.editor.setFontSize("12px");
this.editor.setTheme("ace/theme/monokai");
this.editor.setSession(this.sessions[this.state.currentTab]);
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
this.editor.session.setOption("tabSize", tabSize);
this.editor.on("blur", this._updateCode);
this.interval = setInterval(this._updateCode, 3000);
});
willUnmount() {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
}
willUpdateProps(nextProps) {
this._setupSessions(nextProps);
}
onPatched(() => {
const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
});
patched() {
const session = this.sessions[this.state.currentTab];
let content = this.props[this.state.currentTab];
if (content === false) {
const tab = this.props.js !== false ? "js" : this.props.xml ? "xml" : "css";
content = this.props[tab];
this.state.currentTab = tab;
}
if (this.editor.getValue() !== content) {
session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
onWillUpdateProps((nextProps) => this._setupSessions(nextProps));
onWillUnmount(() => {
clearInterval(this.interval);
this.editor.off("blur", this._updateCode);
});
}
setTab(tab) {
@@ -298,20 +202,20 @@ class TabbedEditor extends owl.Component {
const editorValue = this.editor.getValue();
const propsValue = this.props[this.state.currentTab];
if (editorValue !== propsValue) {
this.trigger("updateCode", {
this.props.updateCode({
type: this.state.currentTab,
value: editorValue
});
}
}
}
TabbedEditor.template = "TabbedEditor";
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
constructor(...args) {
super(...args);
setup() {
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
@@ -326,10 +230,11 @@ class App extends owl.Component {
topPanelHeight: null
});
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
this.runCode = owl.utils.debounce(this.runCode, 250, true);
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = debounce(this.runCode, 250, true);
this.downloadCode = debounce(this.downloadCode, 250, true);
this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
}
runCode() {
@@ -379,9 +284,9 @@ class App extends owl.Component {
}
});
}
updateCode(ev) {
if (this.state[ev.detail.type] !== ev.detail.value) {
this.state[ev.detail.type] = ev.detail.value;
updateCode({type, value}) {
if (this.state[type] !== value) {
this.state[type] = value;
this.isDirty = true;
}
}
@@ -401,12 +306,13 @@ class App extends owl.Component {
async downloadCode() {
const { js, css, xml } = this.state;
const content = await makeApp(js, css, xml);
await owl.utils.loadJS("libs/FileSaver.min.js");
const content = await exportStandaloneApp(js, css, xml);
await loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
}
}
App.components = { TabbedEditor };
App.template = "App";
//------------------------------------------------------------------------------
// Application initialization
@@ -416,12 +322,13 @@ async function start() {
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.utils.loadFile("templates.xml"),
owl.utils.whenReady()
owl.loadFile("templates.xml"),
owl.whenReady()
]);
const qweb = new owl.QWeb({ templates });
const env = { qweb };
await mount(App, {target: document.body, env});
const rootApp = new owl.App(App);
rootApp.addTemplates(templates);
await rootApp.mount(document.body);
}
start();
+123
View File
@@ -0,0 +1,123 @@
import { loadJS } from "./utils.js";
const sourceCache = new Map();
async function getSourceCode(filePath) {
if (sourceCache.has(filePath)) {
return sourceCache.get(filePath);
}
let source = await fetch(`${filePath}.js`);
source = await source.text();
sourceCache.set(filePath, source);
return source;
}
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script type="module" src="app.js"></script>
<script type="module" src="utils.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server():
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
* Make a zip file containing a functioning application
*/
export async function exportStandaloneApp(js, css, xml) {
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
import * as utils from "./utils.js";
async function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
function prepareOWLApp(templates) {
const _configure = owl.App.prototype.configure;
owl.App.prototype.configure = function configureOverriden(config) {
config = Object.assign({ dev: true }, config);
this.addTemplates(templates);
return _configure.call(this, config);
}
}
async function start() {
let templates;
try {
templates = await owl.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
prepareOWLApp(templates);
app();
}
start();
`;
const [owlSrc, utilsSrc] = await Promise.all([
getSourceCode("../owl"),
getSourceCode("./utils"),
]);
zip.file("app.js", JS);
zip.file("utils.js", utilsSrc);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSrc);
return zip.generateAsync({ type: "blob" });
}
+2
View File
@@ -12,6 +12,8 @@
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="export_snippet.js"></script>
<script type="module" src="utils.js"></script>
<script type="module" src="app.js"></script>
</head>
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -1,8 +1,8 @@
<templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="setTab(tab)">
<t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
<t t-esc="tab"/>
</a>
</t>
@@ -12,8 +12,7 @@
<div t-name="App" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle"
t-on-updateCode="updateCode">
t-att-style="leftPaneStyle">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
@@ -28,7 +27,8 @@
js="state.js"
css="!state.splitLayout and state.css"
xml="!state.splitLayout and state.xml"
t-att-style="topEditorStyle"/>
style="topEditorStyle"
updateCode="updateCode"/>
<t t-if="state.splitLayout">
<div class="separator horizontal"/>
<TabbedEditor
@@ -36,7 +36,8 @@
css="state.css"
xml="state.xml"
resizeable="true"
t-on-updatePanelHeight="updatePanelHeight"/>
onUpdatePanelHeight="updatePanelHeight"
updateCode="updateCode"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
+49
View File
@@ -0,0 +1,49 @@
export function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
export function useBus(bus, evName, cb) {
bus.addEventListener(evName, cb);
owl.onWillUnmount(() => {
bus.removeEventListener(evName, cb);
});
}
const loadedScripts = {};
export function loadJS(url) {
if (url in loadedScripts) {
return loadedScripts[url];
}
const promise = new Promise(function (resolve, reject) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function () {
resolve();
};
script.onerror = function () {
reject(`Error loading file '${url}'`);
};
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(script);
});
loadedScripts[url] = promise;
return promise;
}