Compare commits

...

12 Commits

Author SHA1 Message Date
Géry Debongnie c085eef441 experiment: add support for attached components 2024-10-31 07:39:24 +01:00
Géry Debongnie b4f84513a6 [REF] app: reorganize startup code
The goal of this refactoring is to delay the instantiation of the
component node until we are ready to mount the component. This is useful
so we know if a component will be attached or mounted (which means that
it needs to have a template, or not)
2024-10-29 13:49:04 +01:00
Géry Debongnie b8d09e523d [FIX] app: make subroots more robust
This commit fixes two issues with subroots:

1. creating a subroot create a new component node synchronously. This
   would causes issues if the creation was done in the setup of a
component, since in that case, owl would reset the current component to
null right after, which would cause all calls to hooks to fail. This is
fixed by restoring the previous component node right after creating a
root.

2. the destroy method for roots calls the scheduler processTasks.
   However, the processTasks method was not safe to reentrant calls,
which would in some cases crashes owl. For example, if a destroy is done
while a new component is mounted, the mount method would be called
twice.

This is fixed by ignoring the processTasks if we are currently
processing tasks. It works because the "for ... of" loop will still
process all new tasks in the current iteration.
2024-10-21 14:28:27 +02:00
Julien Carion (juca) 04c2808701 [IMP] devtools: show objects class name and add special extension cases
This commit ensures that class names are displayed for non generic
objects and extensions of generic classes will have custom display for
their content.
2024-10-11 10:39:50 +02:00
Julien Carion (juca) 15c2604df1 [IMP] devtools: allow expansion of empty class
This commit makes it possible to expand empty classes to see their
prototype instead of displaying them as simple empty objects.
2024-10-11 10:39:50 +02:00
Romeo Fragomeli 3e11fe6b12 [IMP] Bump support for Node 20+
This commit bump the support for Node 20+.
2024-10-09 20:31:31 +02:00
Romeo Fragomeli 20c6cacb4e [REL] v2.4.0
# v2.4.0

 - [IMP] owl: add basic support for sub roots
 - [IMP] make set of timeout-able hooks (and their timeouts) clearer by using a const map
 - [IMP] devtools: add support for file urls on chrome
2024-09-30 10:49:39 +02:00
Géry Debongnie eb2b32ab60 [IMP] owl: add basic support for sub roots
In this commit, we extend the owl App class to support multiple sub
roots. This is useful for situations where we want to mount sub
components in non-managed DOM. This is exactly what the Knowledge app is
doing, with mounting views in an html editor.

Currently, this requires some difficult and fragile hacks, and still,
the result is that it is very easy to mix components from the main App
and a SubApp.  But Knowledge does not actually care about creating a sub
app. It only needs the possibility to mount sub components in dynamic
places.

closes #1640
2024-09-27 15:35:37 -04:00
Xavier Morel 2a223288d4 [IMP] make set of timeout-able hooks (and their timeouts) clearer by using a const map
Also unnest the handling of `result` via guard clauses, and generate
messages as close as possible to use site, keeping the error
construction itself where it currently is as the goal is specifically
to point back to the *definition* site for the hook function.
2024-09-23 20:29:20 +02:00
Julien Carion (juca) 1272278225 [IMP] devtools: add support for file urls on chrome
This commit ensures that the hook will properly be loaded on file urls
when using chrome (this was already working in firefox). It is still
necessary to check the "Allow access to file URLs" option in the
extensions manager for this to work properly.
2024-09-18 16:24:53 +02:00
Aaron Bohy f502dd732e [REL] v2.3.1
# v2.3.1

 - [FIX] runtime: log if willStart takes more than 3s
2024-08-14 16:25:32 +02:00
Aaron Bohy 9c2d957525 [FIX] runtime: log if willStart takes more than 3s
Before this commit, when willStart/willUpdateProps took more than
3s, a console.warn was done. In odoo, when a warning is logged
during a test, the test fails and the build is considered as "in
error".

There's a component that loads several resources (sequentially) in
its onWillStart, which *sometimes* takes more than 3s, making
builds fail non deterministically. Since a recent change (which
adds another call in the problematic onWillStart), the warning gets
logged quite often.

A quick fix is necessary, so we change the warn into a log, which
won't make build fail.

We may consider alternatives in the future though:
 - add a parameter to onWillStart, to disable the timeout, or to
   specify the delay (which is 3s by default)
 - do not warn in test mode
2024-08-14 15:48:49 +02:00
30 changed files with 1166 additions and 133 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v2
+28
View File
@@ -6,6 +6,7 @@
- [API](#api)
- [Configuration](#configuration)
- [`mount` helper](#mount-helper)
- [Roots](#roots)
- [Loading templates](#loading-templates)
## Overview
@@ -92,6 +93,33 @@ Most of the time, the `mount` helper is more convenient, but whenever one needs
a reference to the actual Owl App, then using the `App` class directly is
possible.
## Roots
An application can have multiple roots. It is sometimes useful to instantiate
sub components in places that are not managed by Owl, such as an html editor
with dynamic content (the Knowledge application in Odoo).
To create a root, one can use the `createRoot` method, which takes two arguments:
- **`Component`**: a component class (Root component of the app)
- **`config (optional)`**: a config object that may contain a `props` object or a
`env` object.
The `createRoot` method returns an object with a `mount` method (same API as
the `App.mount` method), and a `destroy` method.
```js
const root = app.createRoot(MyComponent, { props: { someProps: true } });
await root.mount(targetElement);
// later
root.destroy();
```
Note that, like with owl `App`, it is the responsibility of the code that created
the root to properly destroy it (before it has been removed from the DOM!). Owl
has no way of doing it itself.
## Loading templates
Most applications will need to load templates whenever they start. Here is
+71 -34
View File
@@ -2598,42 +2598,47 @@ class ComponentNode {
}
const TIMEOUT = Symbol("timeout");
const HOOK_TIMEOUT = {
onWillStart: 3000,
onWillUpdateProps: 3000,
};
function wrapError(fn, hookName) {
const error = new OwlError(`The following error occurred in ${hookName}: `);
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
const error = new OwlError();
const timeoutError = new OwlError();
const node = getCurrent();
return (...args) => {
const onError = (cause) => {
error.cause = cause;
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
}
else {
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
}
error.message =
cause instanceof Error
? `The following error occurred in ${hookName}: "${cause.message}"`
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
throw error;
};
let result;
try {
const result = fn(...args);
if (result instanceof Promise) {
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
const fiber = node.fiber;
Promise.race([
result.catch(() => { }),
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
console.warn(timeoutError);
}
});
}
return result.catch(onError);
}
return result;
result = fn(...args);
}
catch (cause) {
onError(cause);
}
if (!(result instanceof Promise)) {
return result;
}
const timeout = HOOK_TIMEOUT[hookName];
if (timeout) {
const fiber = node.fiber;
Promise.race([
result.catch(() => { }),
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;
console.log(timeoutError);
}
});
}
return result.catch(onError);
};
}
// -----------------------------------------------------------------------------
@@ -5552,7 +5557,7 @@ function compile(template, options = {}) {
}
// do not modify manually. This file is generated by the release script.
const version = "2.3.0";
const version = "2.4.0";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5647,6 +5652,7 @@ class App extends TemplateSet {
constructor(Root, config = {}) {
super(config);
this.scheduler = new Scheduler();
this.subRoots = new Set();
this.root = null;
this.name = config.name || "";
this.Root = Root;
@@ -5665,14 +5671,42 @@ class App extends TemplateSet {
this.props = config.props || {};
}
mount(target, options) {
App.validateTarget(target);
if (this.dev) {
validateProps(this.Root, this.props, { __owl__: { app: this } });
const root = this.createRoot(this.Root, { props: this.props });
this.root = root.node;
this.subRoots.delete(root.node);
return root.mount(target, options);
}
createRoot(Root, config = {}) {
const props = config.props || {};
// hack to make sure the sub root get the sub env if necessary. for owl 3,
// would be nice to rethink the initialization process to make sure that
// we can create a ComponentNode and give it explicitely the env, instead
// of looking it up in the app
const env = this.env;
if (config.env) {
this.env = config.env;
}
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
const node = this.makeNode(Root, props);
if (config.env) {
this.env = env;
}
this.subRoots.add(node);
return {
node,
mount: (target, options) => {
App.validateTarget(target);
if (this.dev) {
validateProps(Root, props, { __owl__: { app: this } });
}
const prom = this.mountNode(node, target, options);
return prom;
},
destroy: () => {
this.subRoots.delete(node);
node.destroy();
this.scheduler.processTasks();
},
};
}
makeNode(Component, props) {
return new ComponentNode(Component, props, this, null, null);
@@ -5704,6 +5738,9 @@ class App extends TemplateSet {
}
destroy() {
if (this.root) {
for (let subroot of this.subRoots) {
subroot.destroy();
}
this.root.destroy();
this.scheduler.processTasks();
}
@@ -5981,6 +6018,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2024-07-25T13:13:44.371Z';
__info__.hash = '0cde4b8';
__info__.date = '2024-09-30T08:49:29.420Z';
__info__.hash = 'eb2b32a';
__info__.url = 'https://github.com/odoo/owl';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.3.0",
"version": "2.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.3.0",
"version": "2.4.0",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -9,7 +9,7 @@
"dist"
],
"engines": {
"node": ">=12.18.3"
"node": ">=20.0.0"
},
"scripts": {
"build:bundle": "rollup -c --failAfterWarnings",
@@ -1,4 +1,4 @@
import { OwlError } from "../common/owl_error";
import { OwlError } from "./owl_error";
/**
* Owl QWeb Expression Parser
+1 -1
View File
@@ -4,7 +4,7 @@ import {
interpolate,
INTERP_REGEXP,
replaceDynamicParts,
} from "./inline_expressions";
} from "../common/inline_expressions";
import {
AST,
ASTComment,
+87 -19
View File
@@ -1,14 +1,14 @@
import { OwlError } from "../common/owl_error";
import { version } from "../version";
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node";
import { nodeErrorHandlers, handleError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { Fiber, RootFiber, MountOptions } from "./fibers";
import { ComponentNode, saveCurrent } from "./component_node";
import { handleError, nodeErrorHandlers } from "./error_handling";
import { Fiber, MountOptions, RootFiber } from "./fibers";
import { reactive, toRaw } from "./reactivity";
import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils";
import { toRaw, reactive } from "./reactivity";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -16,10 +16,13 @@ export interface Env {
[key: string]: any;
}
export interface AppConfig<P, E> extends TemplateSetConfig {
name?: string;
export interface RootConfig<P, E> {
props?: P;
env?: E;
}
export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
name?: string;
test?: boolean;
warnIfNoStaticProps?: boolean;
}
@@ -49,6 +52,12 @@ declare global {
}
}
interface Root<P extends Props, E> {
node: ComponentNode<P, E> | null;
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
destroy(): void;
}
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
export class App<
@@ -65,8 +74,10 @@ export class App<
props: P;
env: E;
scheduler = new Scheduler();
root: ComponentNode<P, E> | null = null;
subRoots: Set<Root<any, any>> = new Set();
root: Root<P, E> | null = null;
warnIfNoStaticProps: boolean;
_lastRootEl: HTMLElement | ShadowRoot | null = null; // temporary ref to propagate to roots
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config);
@@ -91,14 +102,64 @@ export class App<
target: HTMLElement | ShadowRoot,
options?: MountOptions
): Promise<Component<P, E> & InstanceType<T>> {
App.validateTarget(target);
if (this.dev) {
validateProps(this.Root, this.props, { __owl__: { app: this } });
}
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
return prom;
this.root = this.createRoot(this.Root, { props: this.props });
return this.root.mount(target, options) as any;
}
createRoot<Props extends object, SubEnv = any>(
Root: ComponentConstructor<Props, E>,
config: RootConfig<Props, SubEnv> = {}
): Root<Props, SubEnv> {
const props = config.props || ({} as Props);
const env = this.env;
const root: Root<Props, SubEnv> = {
node: null,
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
App.validateTarget(target);
// hack to make sure the sub root get the sub env if necessary. for owl 3,
// would be nice to rethink the initialization process to make sure that
// we can create a ComponentNode and give it explicitely the env, instead
// of looking it up in the app
if (config.env) {
this.env = config.env as any;
}
const restore = saveCurrent();
if (options?.position === "attach") {
if (Root.template) {
throw new Error("Cannot attach a component with a template");
}
this._lastRootEl = target;
} else {
if (!Root.template) {
// no template => trigger an error
this.getTemplate("");
}
}
const node = this.makeNode(Root, props);
root.node = node;
this._lastRootEl = null;
restore();
if (config.env) {
this.env = env;
}
if (this.dev) {
validateProps(Root, props, { __owl__: { app: this } });
}
const prom = this.mountNode(node, target, options);
return prom;
},
destroy: () => {
this.subRoots.delete(root);
if (root.node) {
root.node?.destroy();
this.scheduler.processTasks();
}
},
};
this.subRoots.add(root);
return root;
}
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
@@ -133,10 +194,17 @@ export class App<
}
destroy() {
if (this.root) {
this.root.destroy();
this.scheduler.processTasks();
const roots = [...this.subRoots].reverse();
for (let root of roots) {
root.destroy();
}
// if (this.root) {
// for (let subroot of this.subRoots) {
// subroot.destroy();
// }
// this.root.destroy();
this.scheduler.processTasks();
// }
apps.delete(this);
}
+1
View File
@@ -9,6 +9,7 @@ export type Props = { [key: string]: any };
interface StaticComponentProperties {
template: string;
dynamicContent?: { [spec: string]: string };
defaultProps?: any;
props?: Schema;
components?: { [componentName: string]: ComponentConstructor };
+102 -2
View File
@@ -6,10 +6,19 @@ import { OwlError } from "../common/owl_error";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
import { STATUS } from "./status";
import { batched, Callback } from "./utils";
import { batched, Callback, Markup } from "./utils";
import { xml } from "./template_set";
import { compileExpr } from "../common/inline_expressions";
let currentNode: ComponentNode | null = null;
export function saveCurrent() {
let n = currentNode;
return () => {
currentNode = n;
};
}
export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
@@ -117,11 +126,102 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
this.component = new C(props, env, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
if (C.template) {
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
} else {
// component will be attached
this.renderFn = app.getTemplate(xml``).bind(this.component, ctx, this);
if (C.dynamicContent) {
this.prepareAttach(app._lastRootEl!, C.dynamicContent, ctx);
}
}
this.component.setup();
currentNode = null;
}
prepareAttach(
el: HTMLElement | ShadowRoot,
dynamicContent: { [spec: string]: string },
ctx: any
) {
const attrs: { selector: string; attr: string; fn: Function }[] = [];
const handlers: { selector: string; event: string; fn: any }[] = [];
const tOuts: { selector: string; fn: Function }[] = [];
for (let key in dynamicContent) {
const value = dynamicContent[key];
const parts = key.split(":");
if (parts[1].startsWith("t-att-")) {
const attr = parts[1].slice(6);
const fn = new Function("ctx", `return ${compileExpr(value)};`);
attrs.push({
selector: parts[0],
attr,
fn,
});
}
if (parts[1].startsWith("t-on-")) {
const event = parts[1].slice(5);
// const fn = new Function("ctx", "ev", `${compileExpr(value)}(ev);`);
const fn = (ev: any) => (this as any).component[value](ev);
handlers.push({
selector: parts[0],
event,
fn,
});
}
if (parts[1] === "t-out") {
const fn = new Function("ctx", `return ${compileExpr(value)};`);
tOuts.push({ selector: parts[0], fn });
}
}
const handleAttrs = () => {
for (let attr of attrs) {
const val = attr.fn.call(this.component, ctx);
// todo: cache the queryselector result?
const target = attr.selector === "root" ? el : (el.querySelector(attr.selector) as any);
if (target) {
target.setAttribute(attr.attr, val);
}
}
};
const handleEvents = () => {
for (let handler of handlers) {
// const val = attr.fn.call(this.component, ctx);
// todo: cache the queryselector result?
const target =
handler.selector === "root" ? el : (el.querySelector(handler.selector) as any);
if (target) {
target.addEventListener(handler.event, handler.fn);
}
}
};
const handleTOuts = () => {
for (let tOut of tOuts) {
const val = tOut.fn.call(this.component, ctx);
// todo: cache the queryselector result?
const target = tOut.selector === "root" ? el : (el.querySelector(tOut.selector) as any);
if (target) {
if (val instanceof Markup) {
target.innerHTML = val as any;
} else {
target.textContent = val;
}
}
}
};
if (attrs.length) {
this.mounted.push(handleAttrs);
this.patched.push(handleAttrs);
}
if (handlers.length) {
this.mounted.push(handleEvents);
}
if (tOuts.length) {
this.mounted.push(handleTOuts);
this.patched.push(handleTOuts);
}
}
mountComponent(target: any, options?: MountOptions) {
const fiber = new MountFiber(this, target, options);
this.app.scheduler.addFiber(fiber);
+1 -1
View File
@@ -207,7 +207,7 @@ export class RootFiber extends Fiber {
}
}
type Position = "first-child" | "last-child";
type Position = "first-child" | "last-child" | "attach";
export interface MountOptions {
position?: Position;
+17
View File
@@ -136,3 +136,20 @@ export function useExternalListener(
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
// -----------------------------------------------------------------------------
// useAttachedEl
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow attached components to get a reference to
* the element they have been attached on.
*/
export function useAttachedEl(): HTMLElement {
const node = getCurrent();
const el = node.app._lastRootEl as HTMLElement;
if (!el) {
throw new Error("useAttachedEl can only be called with component that are attached");
}
return el;
}
+31 -23
View File
@@ -3,42 +3,50 @@ import { nodeErrorHandlers } from "./error_handling";
import { OwlError } from "../common/owl_error";
const TIMEOUT = Symbol("timeout");
const HOOK_TIMEOUT: { [key: string]: number } = {
onWillStart: 3000,
onWillUpdateProps: 3000,
};
function wrapError(fn: (...args: any[]) => any, hookName: string) {
const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
const error = new OwlError() as Error & {
cause: any;
};
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
const timeoutError = new OwlError();
const node = getCurrent();
return (...args: any[]) => {
const onError = (cause: any) => {
error.cause = cause;
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
} else {
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
}
error.message =
cause instanceof Error
? `The following error occurred in ${hookName}: "${cause.message}"`
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
throw error;
};
let result;
try {
const result = fn(...args);
if (result instanceof Promise) {
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
const fiber = node.fiber;
Promise.race([
result.catch(() => {}),
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
console.warn(timeoutError);
}
});
}
return result.catch(onError);
}
return result;
result = fn(...args);
} catch (cause) {
onError(cause);
}
if (!(result instanceof Promise)) {
return result;
}
const timeout = HOOK_TIMEOUT[hookName];
if (timeout) {
const fiber = node.fiber;
Promise.race([
result.catch(() => {}),
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
]).then((res) => {
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
timeoutError.message = `${hookName}'s promise hasn't resolved after ${
timeout / 1000
} seconds`;
console.log(timeoutError);
}
});
}
return result.catch(onError);
};
}
+6
View File
@@ -16,6 +16,7 @@ export class Scheduler {
frame: number = 0;
delayedRenders: Fiber[] = [];
cancelledNodes: Set<ComponentNode> = new Set();
processing = false;
constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -53,6 +54,10 @@ export class Scheduler {
}
processTasks() {
if (this.processing) {
return;
}
this.processing = true;
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
@@ -66,6 +71,7 @@ export class Scheduler {
this.tasks.delete(task);
}
}
this.processing = false;
}
processFiber(fiber: RootFiber) {
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.3.0";
export const version = "2.4.0";
+27
View File
@@ -43,6 +43,33 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
}"
`;
exports[`app can call processTask twice in a row without crashing 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`parent\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`app can call processTask twice in a row without crashing 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`app can configure an app with props 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -0,0 +1,199 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`destroy a subroot while another component is mounted in main app 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`ChildB\`, true, false, false, []);
const comp2 = app.createComponent(\`ChildA\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
if (ctx['state'].flag) {
b2 = comp1({}, key + \`__1\`, node, this, null);
} else {
b3 = comp2({}, key + \`__2\`, node, this, null);
}
return multi([b2, b3]);
}
}"
`;
exports[`destroy a subroot while another component is mounted in main app 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block3 = createBlock(\`<div block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`a\`);
let ref1 = (el) => this.__owl__.setRef((\`elem\`), el);
const b3 = block3([ref1]);
return multi([b2, b3]);
}
}"
`;
exports[`destroy a subroot while another component is mounted in main app 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`c\`);
}
}"
`;
exports[`destroy a subroot while another component is mounted in main app 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`b\`);
}
}"
`;
exports[`subroot by default, env is the same in sub root 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>main app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot by default, env is the same in sub root 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>sub root</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot can create a root in a setup function, then use a hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`a\`);
}
}"
`;
exports[`subroot can mount subroot 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>main app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot can mount subroot 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>sub root</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot can mount subroot inside own dom 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>main app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot can mount subroot inside own dom 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>sub root</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot env can be specified for sub roots 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>main app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot env can be specified for sub roots 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>sub root</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>main app</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>sub root</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
+18 -1
View File
@@ -1,4 +1,4 @@
import { App, Component, mount, onWillStart, useState, xml } from "../../src";
import { App, Component, mount, onWillPatch, onWillStart, useState, xml } from "../../src";
import { status } from "../../src/runtime/status";
import {
makeTestFixture,
@@ -184,4 +184,21 @@ describe("app", () => {
expect(Object.keys(app.templates)).toEqual(["hello"]);
expect(Object.keys(app.rawTemplates)).toEqual(["hello", "world"]);
});
test("can call processTask twice in a row without crashing", async () => {
class Child extends Component {
static template = xml`<div/>`;
setup() {
onWillPatch(() => app.scheduler.processTasks());
}
}
class SomeComponent extends Component {
static template = xml`parent<Child/>`;
static components = { Child };
}
const app = new App(SomeComponent);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("parent<div></div>");
});
});
+176
View File
@@ -0,0 +1,176 @@
import { App, Component, onMounted, onWillDestroy, useRef, useState, xml } from "../../src";
import { status } from "../../src/runtime/status";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
class SomeComponent extends Component {
static template = xml`<div>main app</div>`;
}
class SubComponent extends Component {
static template = xml`<div>sub root</div>`;
}
describe("subroot", () => {
test("can mount subroot", async () => {
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>main app</div>");
const subRoot = app.createRoot(SubComponent);
const subcomp = await subRoot.mount(fixture);
expect(fixture.innerHTML).toBe("<div>main app</div><div>sub root</div>");
app.destroy();
expect(fixture.innerHTML).toBe("");
expect(status(comp)).toBe("destroyed");
expect(status(subcomp)).toBe("destroyed");
});
test("can mount subroot inside own dom", async () => {
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>main app</div>");
const subRoot = app.createRoot(SubComponent);
const subcomp = await subRoot.mount(fixture.querySelector("div")!);
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
app.destroy();
expect(fixture.innerHTML).toBe("");
expect(status(comp)).toBe("destroyed");
expect(status(subcomp)).toBe("destroyed");
});
test("by default, env is the same in sub root", async () => {
let env, subenv;
class SC extends SomeComponent {
setup() {
env = this.env;
}
}
class Sub extends SubComponent {
setup() {
subenv = this.env;
}
}
const app = new App(SC);
await app.mount(fixture);
const subRoot = app.createRoot(Sub);
await subRoot.mount(fixture);
expect(env).toBeDefined();
expect(subenv).toBeDefined();
expect(env).toBe(subenv);
});
test("env can be specified for sub roots", async () => {
const env1 = { env1: true };
const env2 = {};
let someComponentEnv: any, subComponentEnv: any;
class SC extends SomeComponent {
setup() {
someComponentEnv = this.env;
}
}
class Sub extends SubComponent {
setup() {
subComponentEnv = this.env;
}
}
const app = new App(SC, { env: env1 });
await app.mount(fixture);
const subRoot = app.createRoot(Sub, { env: env2 });
await subRoot.mount(fixture);
// because env is different in app => it is given a sub object, frozen and all
// not sure it is a good idea, but it's the way owl 2 works. maybe we should
// avoid doing anything with the main env and let user code do it if they
// want. in that case, we can change the test here to assert that they are equal
expect(someComponentEnv).not.toBe(env1);
expect(someComponentEnv!.env1).toBe(true);
expect(subComponentEnv).toBe(env2);
});
test("subcomponents can be destroyed, and it properly cleanup the subroots", async () => {
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>main app</div>");
const root = app.createRoot(SubComponent);
const subcomp = await root.mount(fixture.querySelector("div")!);
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
root.destroy();
expect(fixture.innerHTML).toBe("<div>main app</div>");
expect(status(comp)).not.toBe("destroyed");
expect(status(subcomp)).toBe("destroyed");
});
test("can create a root in a setup function, then use a hook", async () => {
class C extends Component {
static template = xml`c`;
}
class A extends Component {
static template = xml`a`;
state: any;
setup() {
app.createRoot(C);
this.state = useState({ value: 1 });
}
}
const app = new App(A);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("a");
});
});
test("destroy a subroot while another component is mounted in main app", async () => {
class C extends Component {
static template = xml`c`;
}
class ChildA extends Component {
static template = xml`a<div t-ref="elem"></div>`;
ref: any;
setup() {
this.ref = useRef("elem");
let root = app.createRoot(C);
onMounted(() => {
root.mount(this.ref.el);
});
onWillDestroy(() => {
root.destroy();
});
}
}
class ChildB extends Component {
static template = xml`b`;
}
class SomeComponent extends Component {
static template = xml`
<t t-if="state.flag"><ChildB/></t>
<t t-else=""><ChildA/></t>
`;
static components = { ChildA, ChildB };
state = useState({ flag: false });
}
const app = new App(SomeComponent);
const comp = await app.mount(fixture);
expect(fixture.innerHTML).toBe("a<div></div>");
await nextTick();
expect(fixture.innerHTML).toBe("a<div>c</div>");
comp.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("b");
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { compileExpr, tokenize } from "../../src/compiler/inline_expressions";
import { compileExpr, tokenize } from "../../src/common/inline_expressions";
describe("tokenizer", () => {
test("simple tokens", () => {
+1 -1
View File
@@ -48,7 +48,7 @@ describe("basic validation", () => {
test("compilation error", () => {
const template = `<div t-att-class="a b">test</div>`;
expect(() => renderToString(template))
.toThrow(`Failed to compile anonymous template: Unexpected identifier
.toThrow(`Failed to compile anonymous template: Unexpected identifier 'ctx'
generated code:
function(app, bdom, helpers) {
@@ -97,6 +97,19 @@ exports[`basics a component cannot be mounted in a detached node (even if node i
}"
`;
exports[`basics a component cannot be mounted in a detached node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics a component inside a component 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -261,6 +274,19 @@ exports[`basics can mount a simple component with props 1`] = `
}"
`;
exports[`basics cannot mount on a documentFragment 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>content</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics child can be updated 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1002,6 +1028,19 @@ exports[`basics three level of components with collapsing root nodes 3`] = `
}"
`;
exports[`basics throws if mounting on target=null 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<span>simple vnode</span>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`basics two child components 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -332,6 +332,31 @@ exports[`lifecycle hooks lifecycle semantics, part 3 1`] = `
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 3 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`GrandChild\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 3 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 4 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -683,7 +708,7 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
}"
`;
exports[`lifecycle hooks timeout in onWillStart doesn't emit a warning if app is destroyed 1`] = `
exports[`lifecycle hooks timeout in onWillStart doesn't emit a console log if app is destroyed 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -696,7 +721,7 @@ exports[`lifecycle hooks timeout in onWillStart doesn't emit a warning if app is
}"
`;
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
exports[`lifecycle hooks timeout in onWillStart emits a console log 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -709,7 +734,7 @@ exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
}"
`;
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -723,7 +748,7 @@ exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
}"
`;
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 2`] = `
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -924,6 +924,20 @@ exports[`props validation props: list of strings 1`] = `
}"
`;
exports[`props validation validate props for root component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['message'];
return block1([txt1]);
}
}"
`;
exports[`props validation validate simple types 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -0,0 +1,258 @@
import { Component, markup, mount, useState, xml } from "../../src";
import { useAttachedEl } from "../../src/runtime/hooks";
import { makeTestFixture, nextTick, steps, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
describe("basics", () => {
test("can attach an empty component", async () => {
fixture.innerHTML = "<div>hello</div>";
class Test extends Component {
setup() {
useLogLifecycle();
}
}
await mount(Test, fixture, { position: "attach" });
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Test:setup",
"Test:willStart",
"Test:willRender",
"Test:rendered",
"Test:mounted",
]
`);
expect(fixture.innerHTML).toBe("<div>hello</div>");
});
test("attaching a component with a template throws", async () => {
fixture.innerHTML = "<div>hello</div>";
class Test extends Component {
static template = xml`hello`;
}
let error: Error | null = null;
try {
await mount(Test, fixture, { position: "attach" });
} catch (e: any) {
error = e;
}
expect(error!.message).toBe("Cannot attach a component with a template");
expect(fixture.innerHTML).toBe("<div>hello</div>");
});
test("can attach a component with simple dynamic content", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "value",
};
value: string = "";
setup() {
this.value = "b";
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="b">hello</p></div>');
});
test("useAttachedEl returns the attached element", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
let el: any = null;
class Test extends Component {
setup() {
el = useAttachedEl();
}
}
await mount(Test, fixture, { position: "attach" });
expect(el).toBe(fixture);
});
test("useAttachedEl throws if component is not attached", async () => {
class Test extends Component {
static template = xml`hello`;
setup() {
useAttachedEl();
}
}
let error: any = null;
try {
await mount(Test, fixture);
} catch (_e: any) {
error = _e;
}
expect(error.message).toBe("useAttachedEl can only be called with component that are attached");
});
test("multiple dynamic attribute", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "value",
"p:t-att-b": "value + 'coucou'",
};
value: string = "";
setup() {
this.value = "b";
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="b" b="bcoucou">hello</p></div>');
});
test("attrs can target root", async () => {
fixture.innerHTML = "<p>hello</p>";
class Test extends Component {
static dynamicContent = {
"root:t-att-a": "value",
};
value: string = "";
setup() {
this.value = "b";
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.outerHTML).toBe('<div a="b"><p>hello</p></div>');
});
test("dynamic attribute is updated on rerender", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "state.value",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
}
const test = await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="1">hello</p></div>');
test.state.value = 2;
await nextTick();
expect(fixture.innerHTML).toBe('<div><p a="2">hello</p></div>');
});
test("t-on-click, basic", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
let ev: Event | null = null;
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "state.value",
"p:t-on-click": "onClick",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
onClick(_ev: any) {
ev = _ev;
this.state.value = 2;
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="1">hello</p></div>');
fixture.querySelector("p")!.click();
expect(ev).toBeInstanceOf(Event);
await nextTick();
expect(fixture.innerHTML).toBe('<div><p a="2">hello</p></div>');
});
test("t-on-click, target root element", async () => {
fixture.innerHTML = "hello";
let click = false;
class Test extends Component {
static dynamicContent = {
"root:t-on-click": "onClick",
};
onClick() {
click = true;
}
}
await mount(Test, fixture, { position: "attach" });
fixture.click();
expect(click).toBe(true);
});
test("t-out, basic", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-out": "state.value",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
});
test("t-out, on root", async () => {
fixture.innerHTML = "hello";
class Test extends Component {
static dynamicContent = {
"root:t-out": "state.value",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
}
expect(fixture.outerHTML).toBe("<div>hello</div>");
await mount(Test, fixture, { position: "attach" });
expect(fixture.outerHTML).toBe("<div>1</div>");
});
test("t-out, with markup", async () => {
fixture.innerHTML = `<p class="p1">hello</p><p class="p2">hello</p>`;
class Test extends Component {
static dynamicContent = {
"p.p1:t-out": "value1",
"p.p2:t-out": "value2",
};
value1: any;
value2: any;
setup() {
this.value1 = "<div>value1</div>";
this.value2 = markup("<div>value2</div>");
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe(
`<p class="p1">&lt;div&gt;value1&lt;/div&gt;</p><p class="p2"><div>value2</div></p>`
);
});
});
+2 -2
View File
@@ -157,7 +157,7 @@ describe("basics", () => {
} catch (e) {
error = e as Error;
}
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
generated code:
function(app, bdom, helpers) {
@@ -182,7 +182,7 @@ function(app, bdom, helpers) {
static components = { Child };
static template = xml`<Child/>`;
}
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
generated code:
function(app, bdom, helpers) {
+19 -19
View File
@@ -112,10 +112,10 @@ describe("lifecycle hooks", () => {
await mount(Test, fixture);
});
test("timeout in onWillStart emits a warning", async () => {
const { warn } = console;
let warnArgs: any[];
console.warn = jest.fn((...args) => (warnArgs = args));
test("timeout in onWillStart emits a console log", async () => {
const { log } = console;
let logArgs: any[];
console.log = jest.fn((...args) => (logArgs = args));
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
@@ -138,17 +138,17 @@ describe("lifecycle hooks", () => {
}
await nextMicroTick();
await nextMicroTick();
expect(console.warn).toHaveBeenCalledTimes(1);
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
expect(console.log).toHaveBeenCalledTimes(1);
expect(logArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
} finally {
console.warn = warn;
console.log = log;
window.setTimeout = setTimeout;
}
});
test("timeout in onWillStart doesn't emit a warning if app is destroyed", async () => {
const { warn } = console;
console.warn = jest.fn();
test("timeout in onWillStart doesn't emit a console log if app is destroyed", async () => {
const { log } = console;
console.log = jest.fn();
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
@@ -172,14 +172,14 @@ describe("lifecycle hooks", () => {
}
await nextMicroTick();
await nextMicroTick();
expect(console.warn).toHaveBeenCalledTimes(0);
expect(console.log).toHaveBeenCalledTimes(0);
} finally {
console.warn = warn;
console.log = log;
window.setTimeout = setTimeout;
}
});
test("timeout in onWillUpdateProps emits a warning", async () => {
test("timeout in onWillUpdateProps emits a console log", async () => {
class Child extends Component {
static template = xml``;
setup() {
@@ -193,9 +193,9 @@ describe("lifecycle hooks", () => {
}
const parent = await mount(Parent, fixture, { test: true });
const { warn } = console;
let warnArgs: any[];
console.warn = jest.fn((...args) => (warnArgs = args));
const { log } = console;
let logArgs: any[];
console.log = jest.fn((...args) => (logArgs = args));
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
@@ -218,12 +218,12 @@ describe("lifecycle hooks", () => {
delete timeoutCbs[id];
}
await tick;
expect(console.warn).toHaveBeenCalledTimes(1);
expect(warnArgs![0]!.message).toBe(
expect(console.log).toHaveBeenCalledTimes(1);
expect(logArgs![0]!.message).toBe(
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
);
} finally {
console.warn = warn;
console.log = log;
window.setTimeout = setTimeout;
}
});
+1 -1
View File
@@ -702,7 +702,7 @@ describe("props validation", () => {
const app = new App(Parent, { test: true });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("12");
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
expect(app.root!.node!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
});
test("props are validated whenever component is updated", async () => {
+1 -1
View File
@@ -14,7 +14,7 @@
"default_popup": "popup_app/popup.html"
},
"permissions": ["scripting", "storage"],
"host_permissions": ["http://*/*", "https://*/*"],
"host_permissions": ["http://*/*", "https://*/*", "file://*"],
"content_security_policy": {
"script-src": "self",
"object-src": "self"
@@ -109,23 +109,34 @@
object(obj) {
const result = [];
let length = 0;
for (const [key, value] of Object.entries(obj)) {
if (length > 25) {
result.push("...");
break;
if (obj instanceof String) {
result[0] = `'${obj.toString()}'`;
} else if (obj instanceof Array) {
return `${obj.constructor.name} ${this.array([...obj])}`;
} else if (obj instanceof Number) {
result[0] = obj.toString();
} else {
for (const [key, value] of Object.entries(obj)) {
if (length > 25) {
result.push("...");
break;
}
const element = key + ": " + this.serializeItem(value);
length += element.length;
result.push(element);
}
for (const key of Object.getOwnPropertySymbols(obj)) {
if (length > 25) {
result.push("...");
break;
}
const element = key.toString() + ": " + this.serializeItem(obj[key]);
length += element.length;
result.push(element);
}
const element = key + ": " + this.serializeItem(value);
length += element.length;
result.push(element);
}
for (const key of Object.getOwnPropertySymbols(obj)) {
if (length > 25) {
result.push("...");
break;
}
const element = key.toString() + ": " + this.serializeItem(obj[key]);
length += element.length;
result.push(element);
if (obj.constructor.name !== "Object") {
return obj.constructor.name + " {" + result.join(", ") + "}";
}
return "{" + result.join(", ") + "}";
},
@@ -823,7 +834,7 @@
child.contentType = "set";
child.hasChildren = true;
break;
case obj instanceof Array:
case obj.constructor.name === "Array":
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
@@ -834,7 +845,9 @@
case obj instanceof Object:
child.contentType = "object";
child.hasChildren =
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
Object.keys(obj).length ||
Object.getOwnPropertySymbols(obj).length ||
obj.constructor.name !== "Object";
break;
default:
child.contentType = typeof obj;