Compare commits

...

10 Commits

Author SHA1 Message Date
Géry Debongnie e402ee62de [REL] v1.0.4 2020-01-21 10:28:50 +01:00
Géry Debongnie 5711eb065d [ADD] tooling: add release script
More work is needed to complete this, but the basics is here, and it is
difficult to test.

closes #612
2020-01-21 10:10:45 +01:00
Géry Debongnie 6739e79aac [ADD] tools: add single file component example to playground 2020-01-21 09:27:30 +01:00
Géry Debongnie 1c8dc97fca [FIX] component: can mount on different target without unmounting
closes #616
2020-01-21 09:26:38 +01:00
Géry Debongnie f9cac94dc7 [REF] tests: split component.test into multiple files 2020-01-21 09:26:38 +01:00
Géry Debongnie c62c3fea19 [FIX] types: improve type of useExternalListener
so it can work on window

closes #613
2020-01-21 08:11:40 +01:00
Géry Debongnie 3845607feb [DOC] store: clarify how a component can find a store
closes #617
2020-01-21 08:08:14 +01:00
Géry Debongnie 3fcbb9ad16 [REL] v1.0.3 2020-01-09 16:27:18 +01:00
Géry Debongnie e04b160bff [REL] v1.0.2
v1.0.2 because I am fighting with npm...
2020-01-09 15:12:32 +01:00
Géry Debongnie bc234d2053 [ADD] tools: add benchmarks for 1.0.0 2020-01-09 14:52:27 +01:00
21 changed files with 8119 additions and 2401 deletions
+3 -1
View File
@@ -23,4 +23,6 @@ package-lock.json
node_modules
# Extras temp file
/tools/owl.js
/tools/owl.js
release-notes.md
+2 -2
View File
@@ -110,8 +110,8 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0.js](https://github.com/odoo/owl/releases/download/v1.0.0/owl.js)
- [owl-1.0.0.min.js](https://github.com/odoo/owl/releases/download/v1.0.0/owl.min.js)
- [owl-1.0.4.js](https://github.com/odoo/owl/releases/download/v1.0.4/owl.js)
- [owl-1.0.4.min.js](https://github.com/odoo/owl/releases/download/v1.0.4/owl.min.js)
Some npm scripts are available:
+13 -1
View File
@@ -208,7 +208,11 @@ Note that getters are not cached.
### Connecting a Component
At some point, we need a way to interact with the store from a component. This
can be done with the help of the three store hooks:
means that the component needs a reference to the store. By default, it looks
for it in the `env.store` key. However, this can be configured with the `useStore`
hook.
Every component-store interactions are done with the help of the three store hooks:
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
@@ -229,6 +233,14 @@ const state = {
const store = new owl.Store({ state, actions });
```
To make it accessible to the complete application, we will put it in the
environment:
```js
// in this example, the root component is App
App.env.store = store;
```
A counter component can then select this value and dispatch an action like this:
```js
+10 -4
View File
@@ -1,11 +1,13 @@
{
"name": "@odoo/owl",
"version": "1.0.0",
"version": "1.0.4",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"prepublish": "npm run buildcommonjs",
"files": ["dist/types/", "dist/owl.js"],
"files": [
"dist/types/",
"dist/owl.js"
],
"engines": {
"node": ">=10.15.3"
},
@@ -21,7 +23,9 @@
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write"
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"publish": "npm run buildcommonjs && npm publish",
"release": "node tools/release.js"
},
"repository": {
"type": "git",
@@ -36,8 +40,10 @@
"dependencies": {},
"devDependencies": {
"@types/jest": "^23.3.12",
"chalk": "^3.0.0",
"cpx": "^1.5.0",
"git-rev-sync": "^1.12.0",
"github-api": "^3.3.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"live-server": "^1.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.0
- Current version: 1.0.4
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+8 -1
View File
@@ -302,7 +302,14 @@ export class Component<T extends Env, Props extends {}> {
const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
if (position !== "self" && this.el!.parentNode !== target) {
// in this situation, we are trying to mount a component on a different
// target. In this case, we need to unmount first, otherwise it will
// not work.
this.unmount();
} else {
return Promise.resolve();
}
}
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
+6 -1
View File
@@ -149,7 +149,12 @@ export function useSubEnv(nextEnv) {
* in the constructor of the OWL component that needs to be notified,
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(target: HTMLElement, eventName: string, handler, eventParams?) {
export function useExternalListener(
target: HTMLElement | typeof window,
eventName: string,
handler,
eventParams?
) {
const boundHandler = handler.bind(Component.current);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
@@ -0,0 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'child'
const _4 = scope['state'].style;
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.style = _4;}};});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, {b:scope['state'].b})
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, utils.toObj(scope['state'].b?'b':''))
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
@@ -257,161 +257,6 @@ exports[`basic widget properties t-key on a component with t-if, and a sibling c
}"
`;
exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'child'
const _4 = scope['state'].style;
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.isDestroyed) {return};w2.el.style=_4;});;
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){vn.elm.style = _4;}};});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, {b:scope['state'].b})
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"ParentWidget\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
context.__owl__.refs = context.__owl__.refs || {};
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, utils.toObj(scope['state'].b?'b':''))
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; vnode.data.hook = {create(_, vn){}};});
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.classObj=_5;
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Child\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
}"
`;
exports[`composition sub components with some state rendered in a loop 1`] = `
"function anonymous(context, extra
) {
File diff suppressed because it is too large Load Diff
+271
View File
@@ -0,0 +1,271 @@
import { Component, Env } from "../../src/component/component";
import { QWeb } from "../../src/qweb/qweb";
import { xml } from "../../src/tags";
import { useState, useRef } from "../../src/hooks";
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("class and style attributes with t-component", () => {
test("class is properly added on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<div class="c"/>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`<div><Child class="a b"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="c a b"></div></div>`);
});
test("empty class attribute is not added on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<span/>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child class=""/></div>`;
static components = { Child };
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
});
test("t-att-class is properly added/removed on widget root el", async () => {
class Child extends Component<any, any> {
static template = xml`<div class="c"/>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`<div><Child t-att-class="{a:state.a, b:state.b}"/></div>`;
static components = { Child };
state = useState({ a: true, b: false });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="c a"></div></div>`);
expect(QWeb.TEMPLATES[ParentWidget.template].fn.toString());
widget.state.a = false;
widget.state.b = true;
await nextTick();
expect(fixture.innerHTML).toBe(`<div><div class="c b"></div></div>`);
});
test("class with extra whitespaces", async () => {
env.qweb.addTemplate(
"ParentWidget",
`<div>
<Child class="a b c d"/>
</div>`
);
class Child extends Component<any, any> {}
class ParentWidget extends Component<any, any> {
static components = { Child };
}
env.qweb.addTemplate("Child", `<div/>`);
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div class="a b c d"></div></div>`);
});
test("t-att-class is properly added/removed on widget root el (v2)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<Child class="a" t-att-class="{ b: state.b }" t-ref="child"/>
</div>
<span t-name="Child" class="c" t-att-class="{ d: state.d }"/>
</templates>`);
class Child extends Component<any, any> {
state = useState({ d: true });
}
class ParentWidget extends Component<any, any> {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("t-att-class is properly added/removed on widget root el (v3)", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget">
<Child class="a" t-att-class="state.b ? 'b' : ''" t-ref="child"/>
</div>
<span t-name="Child" class="c" t-att-class="state.d ? 'd' : ''"/>
</templates>`);
class Child extends Component<any, any> {
state = useState({ d: true });
}
class ParentWidget extends Component<any, any> {
static components = { Child };
state = useState({ b: true });
child = useRef("child");
}
const widget = new ParentWidget();
await widget.mount(fixture);
const span = fixture.querySelector("span")!;
expect(span.className).toBe("c d a b");
widget.state.b = false;
await nextTick();
expect(span.className).toBe("c d a");
(widget.child.comp as Child).state.d = false;
await nextTick();
expect(span.className).toBe("c a");
widget.state.b = true;
await nextTick();
expect(span.className).toBe("c a b");
(widget.child.comp as Child).state.d = true;
await nextTick();
expect(span.className).toBe("c a b d");
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("class on components do not interfere with user defined classes", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="App" t-att-class="{ c: state.c }" />
</templates>`);
class App extends Component<any, any> {
state = useState({ c: true });
mounted() {
this.el!.classList.add("user");
}
}
const widget = new App();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe('<div class="c user"></div>');
widget.state.c = false;
await nextTick();
expect(fixture.innerHTML).toBe('<div class="user"></div>');
});
test("style is properly added on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" style="font-weight: bold;"/>
</div>`
);
class SomeComponent extends Component<any, any> {
static template = xml`<div/>`;
}
class ParentWidget extends Component<any, any> {
static components = { child: SomeComponent };
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(`<div><div style="font-weight: bold;"></div></div>`);
});
test("dynamic t-att-style is properly added and updated on widget root el", async () => {
env.qweb.addTemplate(
"ParentWidget",
`
<div>
<t t-component="child" t-att-style="state.style"/>
</div>`
);
class SomeComponent extends Component<any, any> {
static template = xml`<div/>`;
}
class ParentWidget extends Component<any, any> {
static components = { child: SomeComponent };
state = useState({ style: "font-size: 20px" });
}
const widget = new ParentWidget();
await widget.mount(fixture);
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 20px;"></div></div>`);
widget.state.style = "font-size: 30px";
await nextTick();
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 30px;"></div></div>`);
});
test("error in subcomponent with class", async () => {
class Child extends Component<any, any> {
static template = xml`<div t-esc="this.will.crash"/>`;
}
class ParentWidget extends Component<any, any> {
static template = xml`<div><Child class="a"/></div>`;
static components = { Child };
}
const widget = new ParentWidget();
let error;
try {
await widget.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(fixture.innerHTML).toBe("");
});
});
File diff suppressed because it is too large Load Diff
+523
View File
@@ -0,0 +1,523 @@
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("component error handling (catchError)", () => {
/**
* This test suite requires often to wait for 3 ticks. Here is why:
* - First tick is to let the app render and crash.
* - When we crash, we call the catchError handler in a setTimeout (because we
* need to wait for the previous rendering to be completely stopped). So, we
* need to wait for the second tick.
* - Then, when the handler changes the state, we need to wait for the interface
* to be rerendered.
* */
test("can catch an error in a component render function", async () => {
const consoleError = console.error;
console.error = jest.fn();
const handler = jest.fn();
env.qweb.on("error", null, handler);
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("no component catching error lead to full app destruction", async () => {
expect.assertions(6);
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class App extends Component<any, any> {
static template = xml`<div><ErrorComponent flag="state.flag"/></div>`;
static components = { ErrorComponent };
state = useState({ flag: false });
async render() {
try {
await super.render();
} catch (e) {
expect(e.message).toBe("Cannot read property 'this' of undefined");
}
}
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(app.__owl__.isDestroyed).toBe(true);
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the initial call of a component render function (parent updated)", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<ErrorBoundary t-if="state.flag"><ErrorComponent /></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the constructor call of a component render function", async () => {
const handler = jest.fn();
env.qweb.on("error", null, handler);
const consoleError = console.error;
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Component<any, any> {
constructor(parent) {
super(parent);
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
expect(handler).toBeCalledTimes(1);
});
test("can catch an error in the willStart call", async () => {
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div t-name="ErrorComponent">Some text</div>`;
async willStart() {
// we wait a little bit to be in a different stack frame
await nextTick();
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`<div><ErrorBoundary><ErrorComponent /></ErrorBoundary></div>`;
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test.skip("can catch an error in the mounted call", async () => {
// we do not catch error in mounted anymore
console.error = jest.fn();
env.qweb.addTemplates(`
<templates>
<div t-name="ErrorBoundary">
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>
<div t-name="ErrorComponent">Some text</div>
<div t-name="App">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>
</templates>`);
class ErrorComponent extends Component<any, any> {
mounted() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
});
test.skip("can catch an error in the willPatch call", async () => {
// we do not catch error in willPatch anymore
const consoleError = console.error;
console.error = jest.fn();
class ErrorComponent extends Component<any, any> {
static template = xml`<div><t t-esc="props.message"/></div>`;
willPatch() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component<any, any> {
static template = xml`
<div>
<span><t t-esc="state.message"/></span>
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>`;
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
expect(console.error).toHaveBeenCalledTimes(1);
console.error = consoleError;
});
test("a rendering error will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component<any, any> {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in mounted call will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div>abc</div>`;
mounted() {
throw new Error("boom");
}
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in willPatch call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
willPatch() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("an error in patched call will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
class App extends Component<any, any> {
static template = xml`<div><t t-esc="val"/></div>`;
val = 3;
patched() {
throw new Error("boom");
}
}
const app = new App();
await app.mount(fixture);
app.val = 4;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error in a sub component will reject the mount promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
class App extends Component<any, any> {
static template = xml`<div><Child/></div>`;
static components = { Child };
}
const app = new App();
let error;
try {
await app.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise", async () => {
const consoleError = console.error;
console.error = jest.fn(() => {});
// we do not catch error in willPatch anymore
class App extends Component<any, any> {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div></div>");
app.flag = true;
let error;
try {
await app.render();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
test("a rendering error will reject the render promise (with sub components)", async () => {
class Child extends Component<any, any> {
static template = xml`<span></span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child/><t t-esc="x.y"/></div>`;
static components = { Child };
}
let error;
try {
const parent = new Parent();
await parent.mount(fixture);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'y' of undefined");
});
});
+409
View File
@@ -0,0 +1,409 @@
import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
describe("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
await app.mount(div, { position: "self" });
expect(fixture.innerHTML).toBe("<div>app</div>");
});
test("cannot attach a component to an existing node (if not same tagname)", async () => {
class App extends Component<any, any> {
static template = xml`<span>app</span>`;
}
const div = document.createElement("div");
fixture.appendChild(div);
const app = new App();
let error;
try {
await app.mount(div, { position: "self" });
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
});
test("can mount a component (with position='first-child')", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
test("can mount a component (with position='last-child')", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("default mount option is 'last-child'", async () => {
class App extends Component<any, any> {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
describe("unmounting and remounting", () => {
test("widget can be unmounted and remounted", async () => {
const steps: string[] = [];
class MyWidget extends Component<any, any> {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
patched() {
throw new Error("patched should not be called");
}
}
const w = new MyWidget();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
w.unmount();
expect(fixture.innerHTML).toBe("");
expect(steps).toEqual(["willstart", "mounted", "willunmount"]);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]);
});
test("widget can be mounted twice without ill effect", async () => {
const steps: string[] = [];
class MyWidget extends Component<any, any> {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
}
const w = new MyWidget();
await w.mount(fixture);
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div>Hey</div>");
expect(steps).toEqual(["willstart", "mounted"]);
});
test("state changes in willUnmount do not trigger rerender", async () => {
const steps: string[] = [];
class Child extends Component<any, any> {
static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`;
state = useState({ n: 2 });
__render(f) {
steps.push("render");
return super.__render(f);
}
willPatch() {
steps.push("willPatch");
}
patched() {
steps.push("patched");
}
willUnmount() {
steps.push("willUnmount");
this.state.n = 3;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<Child t-if="state.flag" val="state.val"/>
</div>
`;
static components = { Child };
state = useState({ val: 1, flag: true });
}
const widget = new Parent();
await widget.mount(fixture);
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
widget.state.flag = false;
await nextTick();
// we make sure here that no call to __render is done
expect(steps).toEqual(["render", "willUnmount"]);
});
test("state changes in willUnmount will be applied on remount", async () => {
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
willUnmount() {
this.state.val = 3;
}
}
const widget = new TestWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
widget.unmount();
expect(fixture.innerHTML).toBe("");
await nextTick(); // wait for changes to be detected before remounting
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
// we want to make sure that there are no remaining tasks left at this point.
expect(Component.scheduler.tasks.length).toBe(0);
});
test("sub component is still active after being unmounted and remounted", async () => {
class Child extends Component<any, any> {
static template = xml`
<p t-on-click="state.value++">
<t t-esc="state.value"/>
</p>`;
state = useState({ value: 1 });
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><Child/></div>`;
}
const w = new Parent();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
w.unmount();
await nextTick();
await w.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
fixture.querySelector("p")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
});
test("change state just before mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
const widget = new TestWidget();
widget.state.val = 2;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
widget.state.val = 3;
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 3]);
});
test("change state while mounting component", async () => {
const steps: number[] = [];
class TestWidget extends Component<any, any> {
static template = xml`
<div><t t-esc="state.val"/></div>
`;
state = useState({ val: 1 });
__render(f) {
steps.push(this.state.val);
return super.__render(f);
}
}
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch);
const widget = new TestWidget();
let prom = widget.mount(fixture);
widget.state.val = 2;
await prom;
expect(fixture.innerHTML).toBe("<div>2</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
// unmount and re-mount, as in this case, willStart won't be called, so it's
// slightly different
widget.unmount();
prom = widget.mount(fixture);
widget.state.val = 3;
await prom;
expect(fixture.innerHTML).toBe("<div>3</div>");
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3);
expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2);
expect(steps).toEqual([2, 2, 3]);
});
test("change state while component is unmounted", async () => {
let child;
class Child extends Component<any, any> {
static template = xml`<span t-esc="state.val"/>`;
state = useState({
val: "C1"
});
constructor(parent, props) {
super(parent, props);
child = this;
}
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><t t-esc="state.val"/><Child/></div>`;
state = useState({ val: "P1" });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>P1<span>C1</span></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
parent.state.val = "P2";
child.state.val = "C2";
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
});
test("unmount component during a re-rendering", async () => {
const def = makeDeferred();
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
willUpdateProps() {
return def;
}
}
Child.prototype.__render = jest.fn(Child.prototype.__render);
class Parent extends Component<any, any> {
static template = xml`<div><Child val="state.val"/></div>`;
static components = { Child };
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect(Child.prototype.__render).toBeCalledTimes(1);
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.unmount();
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(Child.prototype.__render).toBeCalledTimes(1);
});
test("widget can be mounted on different target", async () => {
const steps: string[] = [];
class MyWidget extends Component<any, any> {
static template = xml`<div>Hey</div>`;
async willStart() {
steps.push("willstart");
}
mounted() {
steps.push("mounted");
}
willUnmount() {
steps.push("willunmount");
}
patched() {
throw new Error("patched should not be called");
}
}
const div = document.createElement("div");
const span = document.createElement("span");
fixture.appendChild(div);
fixture.appendChild(span);
const w = new MyWidget();
await w.mount(div);
expect(fixture.innerHTML).toBe("<div><div>Hey</div></div><span></span>");
await w.mount(span);
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
});
+154
View File
@@ -0,0 +1,154 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = useState({ counter: 0 });
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App();
app.mount(document.body);
}
start();
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL v1.0.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+1
View File
@@ -37,6 +37,7 @@
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-0.24.0">OWL 0.24.0</a></li>
<li><a href="benchmarks/owl-1.0.0-beta1">OWL 1.0.0-beta1</a></li>
<li><a href="benchmarks/owl-1.0.0">OWL 1.0.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+47
View File
@@ -1811,6 +1811,49 @@ const WMS_CSS = `body {
font-size: 20px;
}`;
const SFC = `// This example illustrates how Owl enables single file components,
// which include code, template and style.
//
// This is very useful in some situations, such as testing or quick prototyping.
// Note that this example has no external xml or css file, everything is
// contained in a single js file.
const { Component, useState, tags } = owl;
const { xml, css } = tags;
// Counter component
const COUNTER_TEMPLATE = xml\`
<button t-on-click="state.value++">
Click! [<t t-esc="state.value"/>]
</button>\`;
const COUNTER_STYLE = css\`
button {
color: blue;
}\`;
class Counter extends Component {
state = useState({ value: 0})
}
Counter.template = COUNTER_TEMPLATE;
Counter.style = COUNTER_STYLE;
// App
const APP_TEMPLATE = xml\`
<div>
<Counter/>
<Counter/>
</div>\`;
class App extends Component {}
App.template = APP_TEMPLATE;
App.components = { Counter };
// Application setup
const app = new App();
app.mount(document.body);
`;
export const SAMPLES = [
{
description: "Components",
@@ -1823,6 +1866,10 @@ export const SAMPLES = [
code: FORM,
xml: FORM_XML
},
{
description: "Single File Components",
code: SFC
},
{
description: "Animations",
code: ANIMATION,
+215
View File
@@ -0,0 +1,215 @@
const package = require("../package.json");
const readline = require("readline");
const fs = require("fs");
const exec = require("child_process").exec;
const chalk = require("chalk");
const GitHub = require("github-api");
const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 10;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
startRelease().then(() => {
rl.close();
});
// -----------------------------------------------------------------------------
// Relase Script
// -----------------------------------------------------------------------------
async function startRelease() {
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
// ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`);
const current = package.version;
const next = await ask("Next version: ");
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE;
let content;
try {
content = await readFile("./" + file);
} catch (e) {
logSubContent(e.message);
log("Cannot find release notes... Aborting");
return;
}
// Todo: add playground update feature
// let shouldUpdateStr = await ask("Update Playground? (y/n)");
// const shouldUpdatePlayground = shouldUpdateStr === "y";
const token = await ask("Github token: ");
// ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`);
const testsResult = await execCommand("npm run test");
if (testsResult !== 0) {
log("Test suite does not pass. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`);
await replaceInFile("./package.json", current, next);
await replaceInFile("./README.md", current, next);
await replaceInFile("./roadmap.md", current, next);
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`);
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`);
if (gitResult !== 0) {
log("Git commit failed. Aborting.");
return;
}
ask("Ready for next step...");
// -------------------j--------------------------------------------------------
log(`Step 5/${STEPS}: building owl (iife version)...`);
const buildResult = await execCommand("npm run build");
if (buildResult !== 0) {
log("Build failed. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: minifying owl...`);
const minifyResult = await execCommand("npm run minify");
if (minifyResult !== 0) {
log("Minify failed. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 7/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push");
if (pushResult !== 0) {
log("git push failed. Aborting.");
return;
}
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 8/${STEPS}: publishing release notes on github...`);
const options = {
tag_name: `v${next}`,
name: `v${next}`,
body: content,
draft: true // todo: remove this someday
};
const result = await createRelease(token, options);
console.log(result);
ask("Ready for next step...");
// ---------------------------------------------------------------------------
log(`Step 9/${STEPS}: adding assets to release...`);
await ask("Please add owl.js and owl.min.js to draft release, then confirm");
// todo: do this with curl
// curl \
// -H "Authorization: token $GITHUB_TOKEN" \
// -H "Content-Type: $(file -b --mime-type $FILE)" \
// --data-binary @$FILE \
// "https://uploads.github.com/repos/hubot/singularity/releases/123/assets?name=$(basename $FILE)"
// ---------------------------------------------------------------------------
log(`Step 10/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish");
log("Owl Release process completed! Thank you for your patience");
}
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
function log(text) {
console.log(chalk.yellow(formatLog(text)));
}
function formatLog(text) {
return `[REL] ${text}`;
}
function logSubContent(text) {
for (let line of text.split("\n")) {
if (line.trim()) {
console.log(" " + line);
}
}
}
function ask(question) {
return new Promise(resolve => {
rl.question(question, result => {
resolve(result);
});
});
}
function logStream(stream) {
stream.on("data", data => {
logSubContent(data);
});
}
function execCommand(command) {
return new Promise(resolve => {
const childProcess = exec(command, (err, stdout, stderr) => {
if (err) {
resolve(err.code);
}
});
childProcess.on("exit", code => {
resolve(code);
});
logStream(childProcess.stdout);
logStream(childProcess.stderr);
});
}
function readFile(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, "utf8", function(err, content) {
if (err) {
reject(err);
} else {
resolve(content);
}
});
});
}
async function replaceInFile(file, from, to) {
const content = await readFile(file);
return new Promise((resolve, reject) => {
const updatedContent = content.replace(new RegExp(from, "g"), to);
fs.writeFile(file, updatedContent, "utf8", err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function createRelease(token, options) {
return new Promise((resolve, reject) => {
var gh = new GitHub({ token });
gh.getRepo("odoo", "owl").createRelease(options, (err, result, req) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}