Compare commits

...

20 Commits

Author SHA1 Message Date
Géry Debongnie 4e3b7c74da [REL] v1.4.7
v1.4.7

fix: memory leak in some templates
2021-10-19 16:47:20 +02:00
Géry Debongnie 98bb2842d8 [FIX] run prettier on codebase 2021-10-19 16:45:04 +02:00
Géry Debongnie 6e185f987b [FIX] qweb: do not leak values in global context in rare cases
In some very rare cases (such as the use of the t-foreach directive),
Owl did leak the values in the render context in the global context.
This was due to the fact that the compiled template looked like this:

let _3 = _4 = _5;

instead of

let _3 = _5;
let _4 = _5;
2021-10-19 16:41:45 +02:00
Géry Debongnie c0f495661d [REL] v1.4.6
# v1.4.6

- fix: crash in component render (in rare cases)
- fix: build system now target ES2017 instead of ESNext
- fix: remove useless log in prod mode
2021-10-04 15:16:01 +02:00
Géry Debongnie 0f7a8289a6 [FIX] config: do not log anything if in prod mode
It is worse having a dev mode in a production environment than the
opposite. The purpose of the warning was to avoid that situation.

However, in prod mode, it is not really necessary to log the config key.

closes #915
2021-10-04 14:57:30 +02:00
Géry Debongnie 1c3b04f6a8 [FIX] build: fix typescript target to ES2017
This makes sure that we do not leak modern code in odoo.
2021-10-04 14:57:30 +02:00
Géry Debongnie 64db7777dd [FIX] component: async issue
This is a tricky commit. The key point is that the Fiber.complete
method, which commits a rendering to the DOM works like this: it
traverses the component tree, patch the corresponding DOM for each
component, calls the mounted/destroy hooks, and reset the currentfiber
of components to null, all synchronously.

However, this means that it is possible for components to initiate a
rendering (which create a new currentFiber) before the currentFiber is
reset to null, so the internal state of owl is corrupted. This can
occurs in a crash, as in the test that accompanies this commit.

To fix this, we take care of resetting the currentFiber first, while we
walk the component tree. Then, the internal state is always consistent
(i.e. a currentFiber to null means that there is no pending rendering)

closes #904
2021-10-04 14:57:30 +02:00
Géry Debongnie 37313c47a3 [REL] v1.4.5
# v1.4.5

- qweb: expose translatable attributes
- ci/tests: update jest/rollup
- doc: clarify tutorial
- doc: update readme, add nice badges
2021-09-22 16:34:02 +02:00
Géry Debongnie 3f563de9c0 [CLEANUP] run prettier on codebase 2021-09-22 16:29:59 +02:00
Géry Debongnie 97564a7612 [FIX] tests: update tests error messages
Updating my node version to v16, I noticed many tests breaking, because
the way errors are formatted did change.  This commit ensures that the
test suite keeps working for all node versions.
2021-09-22 16:00:37 +02:00
Géry Debongnie f1abf7f2ea [DOC] update readme, to add nice badges 2021-09-21 09:36:15 +02:00
Géry Debongnie 8464a1b04e [IMP] qweb: expose translatable attributes
This commit allows owl users to add any attribute to the list of
translatable attributes.

closes #903
2021-09-21 09:36:15 +02:00
Géry Debongnie c0a62dfd05 [DOC] clarify tutorial
The part about setting up the various files did not express clearly that
we need the iife file, not the cjs.
2021-09-21 09:36:15 +02:00
Lucas Lefèvre 600f1e35d4 [IMP] package.json: update jest and rollup
If we want to upgrade Typescript to version >4 (in this repo or another using
owl), we also need jest >25. Jest <26 do not support Typescript >4.

However, jest >26 has a few breaking changes that completely breaks owl in
tests.

This commit updates jest to the current latest version (27) and adapts the
code accordingly.

A few words on what changed
---------------------------

1. the default test environment is no longer `jsdom`. It is now manually
   configured to restore the previous env.

2. the jsdom version has been upgraded. This brings a few breaking changes,
   detailed later.

3. there's a bug in jsdom >16.4. Manually created `<t/>` (by qweb compilation)
   are recognized as "T" and not "t". Qweb thinks it's a component (since the
   first letter is capitalized), but it's not a component: boom, everything
   breaks.
   A fix has been proposed here jsdom/jsdom#3240. But it's not likely to land
   in jest in the short term (jest would need to update its dependency to the
   next major jsdom version). This commit works around the problem for now by
   creating `<t/>` slighty differently such that they are in an XML document
   from the start (and not HTML document).

4. the xml parser implementation changed to increase the strictness and
   correctness of XML parsing, according to specifications. A few tests needed
   to be adapted.
   https://github.com/jsdom/jsdom/commit/c96decf837ece54bdc550dfb7dca7e5d6c97bc2d

5. jest matcher `toHaveProperty` now check inherited properties. This breaks a
   few tests. Since the breaking assertions didn't bring a lot of value from a
   behavior point of view (it was more "white box" technical tests), they are
   removed in this commit
   https://github.com/facebook/jest/commit/1256f76a5a83034b51c7524142b60b099f69a7ab

6. jsdom now implements the behavior of links (`<a/>`). One test was `click`ing
   on such a link, with the right click. Jsdom now tries to navigate to the
   pointed URl...but crashes because navigation is not implemented :(
   It turns out the test is probably not a valid/realistic scenario. On every
   tested browser (chrome - chromium - brave - edge -firefox - safari), a
   right click with the mouse triggers a `contextmenu` event and no `click`
   event.
   https://github.com/jsdom/jsdom/commit/cc95abc576f596ff7f3eaf8245f376e1f21aa485
2021-09-08 11:47:50 +02:00
Géry Debongnie b3181f119d [REL] v1.4.4
# v1.4.4

fix: overlapping multi-class in t-att-class
2021-09-03 09:57:00 +02:00
Géry Debongnie 1e1e05350a [REF] run prettier on codebase 2021-09-03 09:56:09 +02:00
Samuel Degueldre b242230e20 [FIX] qweb: fix overlapping multi-class in t-att-class
Previously, if two attributes in t-att-class shared some classes, their
presence would be determined by the last attribute declared, instead of
being present if any attribute containing it evaluates to true. This
commit fixes that.
2021-09-01 11:58:45 +02:00
Géry Debongnie 9fe2da704e [REL] v1.4.3
# v1.4.3

- fix: another scoping issue with t-slots
2021-07-08 11:54:32 +02:00
Géry Debongnie 21b1661d39 [CLEANUP] run prettier on codebase 2021-07-08 11:53:48 +02:00
Géry Debongnie ec05b1f5e3 [FIX] slots: fix bad interaction between t-call and slots
The previous changes in the combine method (used to copy all the
variables defined in the current scope for use in a slot) had the effect
of squashing the prototype chain: instead of `Component -> Obj1 ->
Obj2 -> Obj3`, the combined scope had: `Component -> Obj1'`.

This has an unfortunate interaction with the way t-call is implemented,
which uses the fact that we are in a subscope to add a own
__access_mode__. It depends specifially on the prototype chain, and that
the parent scope may have a different value for that property. But with
the way combine was implemented, we lost all that subtlety since
everything is squashed.

In this commit, we reimplement that function in a way to make sure we
keep the prototype chain structure
2021-07-08 11:49:25 +02:00
26 changed files with 391 additions and 84 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x]
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
+5 -1
View File
@@ -1,5 +1,9 @@
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![npm version](https://badge.fury.io/js/@odoo%2Fowl.svg)](https://badge.fury.io/js/@odoo%2Fowl)
[![Downloads](https://img.shields.io/npm/dm/@odoo%2Fowl.svg)](https://www.npmjs.com/package/@odoo/owl)
_Class based components with hooks, reactive state and concurrent mode_
## Project Overview
@@ -120,7 +124,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.2](https://github.com/odoo/owl/releases/tag/v1.4.2)
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
## License
+3 -1
View File
@@ -70,7 +70,9 @@ just put the following code:
Note that we put everything inside an immediately executed function to avoid leaking
anything to the global scope.
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer).
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
are built to run directly on the browser (other files such as `owl.cjs.js` are
built to be bundled by other tools).
Now, the project should be ready. Loading the `index.html` file into a browser
should show an empty page, with the title `Owl Todo App`, and it should log a
+8 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.4.2",
"version": "1.4.7",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
@@ -37,28 +37,29 @@
},
"homepage": "https://github.com/odoo/owl#readme",
"devDependencies": {
"@types/jest": "^23.3.14",
"@types/jest": "^27.0.1",
"@types/node": "^14.11.8",
"chalk": "^3.0.0",
"cpx": "^1.5.0",
"current-git-branch": "^1.1.0",
"git-rev-sync": "^1.12.0",
"github-api": "^3.3.0",
"jest": "^23.6.0",
"jest-environment-jsdom": "^24.7.1",
"jest": "^27.1.0",
"jest-environment-jsdom": "^27.1.0",
"live-server": "^1.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.4",
"rollup": "^1.6.0",
"rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.27.3",
"rollup-plugin-typescript2": "^0.30.0",
"sass": "^1.16.1",
"source-map-support": "^0.5.10",
"ts-jest": "^23.10.5",
"ts-jest": "^27.0.5",
"typescript": "^3.7.2",
"uglify-es": "^3.3.9"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src",
"<rootDir>/tests"
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.4.2
- Current version: 1.4.7
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+1 -1
View File
@@ -400,6 +400,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
@@ -505,7 +506,6 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const __owl__ = this.__owl__;
__owl__.status = STATUS.MOUNTED;
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
+1 -4
View File
@@ -198,6 +198,7 @@ export class Fiber {
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function (f) {
f.component.__owl__.currentFiber = null;
patchQueue.push(f);
return f.child;
};
@@ -255,10 +256,6 @@ export class Fiber {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
const compOwl = component.__owl__;
if (fiber === compOwl.currentFiber) {
compOwl.currentFiber = null;
}
}
// insert into the DOM (mount case)
+5 -3
View File
@@ -1,4 +1,5 @@
import { QWeb } from "./qweb/index";
import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
/**
* This file creates and exports the OWL 'config' object, with keys:
@@ -9,9 +10,12 @@ import { QWeb } from "./qweb/index";
interface Config {
mode: string;
enableTransitions: boolean;
translatableAttributes: string[];
}
export const config = {} as Config;
export const config = {
translatableAttributes: TRANSLATABLE_ATTRS,
} as Config;
Object.defineProperty(config, "mode", {
get() {
@@ -24,8 +28,6 @@ Object.defineProperty(config, "mode", {
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
},
});
+2 -1
View File
@@ -326,7 +326,8 @@ QWeb.addDirective({
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addLine(`let _${keysID} = _${arrayID};`);
ctx.addLine(`let _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
+32 -7
View File
@@ -66,7 +66,7 @@ interface QWebConfig {
// Const/global stuff/helpers
//------------------------------------------------------------------------------
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
@@ -127,22 +127,36 @@ const UTILS: Utils = {
return result;
}
// this is already an object, but we may need to split keys:
// {'a': true, 'b c': true} should become {a: true, b: true, c: true}
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
for (let key in expr) {
const value = expr[key];
const words = key.split(/\s+/);
for (let word of words) {
result[word] = value;
result[word] = result[word] || value;
}
}
return result;
},
/**
* This method combines the current context with the variables defined in a
* scope for use in a slot.
*
* The implementation is kind of tricky because we want to preserve the
* prototype chain structure of the cloned result. So we need to traverse the
* prototype chain, cloning each level respectively.
*/
combine(context, scope) {
const clone = Object.create(context);
let clone = context;
const scopeStack = [];
while (!isComponent(scope)) {
Object.assign(clone, scope);
scopeStack.push(scope);
scope = scope.__proto__;
}
while (scopeStack.length) {
let scope = scopeStack.pop();
clone = Object.create(clone);
Object.assign(clone, scope);
}
return clone;
},
shallowEqual,
@@ -315,6 +329,9 @@ export class QWeb extends EventBus {
* template, with the name given by the t-name attribute.
*/
addTemplates(xmlstr: string | Document) {
if (!xmlstr) {
return;
}
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
const templates = doc.getElementsByTagName("templates")[0];
if (!templates) {
@@ -543,7 +560,11 @@ export class QWeb extends EventBus {
}
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
const tCallNode = document.createElement("t");
const tCallNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
node.removeAttribute("t-call");
node.prepend(tCallNode);
@@ -579,7 +600,11 @@ export class QWeb extends EventBus {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
const tNode = document.createElement("t");
const tNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
null
).documentElement;
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
for (let child of Array.from(node.childNodes)) {
tNode.appendChild(child);
@@ -51,7 +51,8 @@ exports[`basic widget properties reconciliation alg works for t-foreach in t-for
let vn1 = h('div', p1, c1);
let _2 = scope['state'].s;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -68,7 +69,8 @@ exports[`basic widget properties reconciliation alg works for t-foreach in t-for
let key1 = i1;
let _6 = scope['section'].blips;
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -309,7 +311,8 @@ exports[`composition sub components with some state rendered in a loop 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -597,7 +600,8 @@ exports[`other directives with t-component t-on expression captured in t-foreach
scope.iter = 0;
let _2 = scope['arr'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -639,7 +643,8 @@ exports[`other directives with t-component t-on expression in t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['state'].values;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -691,7 +696,8 @@ exports[`other directives with t-component t-on expression in t-foreach with t-s
scope.bossa = 'nova';
let _2 = scope['state'].values;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -744,7 +750,8 @@ exports[`other directives with t-component t-on method call in t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['state'].values;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -1410,7 +1417,8 @@ exports[`other directives with t-component t-set outside modified in t-foreach 1
scope.iter = 0;
let _2 = scope['state'].values;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -1560,7 +1568,8 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument
let vn1 = h('div', p1, c1);
let _2 = scope['items'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -1838,7 +1847,8 @@ exports[`t-model directive in a t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['state'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -1878,7 +1888,8 @@ exports[`t-model directive in a t-foreach, part 2 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['state'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -414,7 +414,8 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
let _4 = _3;
let _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
@@ -512,7 +513,8 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
c1.push(vn2);
let _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
let _4 = _3;
let _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
@@ -645,7 +647,8 @@ exports[`t-slot directive slots in t-foreach in t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['tree'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -672,7 +675,8 @@ exports[`t-slot directive slots in t-foreach in t-foreach 1`] = `
c1.push(vn8);
let _9 = scope['node1'].nodes;
if (!_9) { throw new Error('QWeb error: Invalid loop expression')}
let _10 = _11 = _9;
let _10 = _9;
let _11 = _9;
if (!(_9 instanceof Array)) {
_10 = Object.keys(_9);
_11 = Object.values(_9);
+56
View File
@@ -1388,6 +1388,62 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<span>4</span>");
});
test("calling render in destroy", async () => {
let a: any = null;
let c: any = null;
class C extends Component {
static template = xml`
<div>
<t t-esc="props.fromA"/>
</div>`;
}
let flag = false;
class B extends Component {
static template = xml`<C fromA="props.fromA"/>`;
static components = { C };
setup() {
c = this;
}
mounted() {
if (flag) {
this.render();
} else {
flag = true;
}
}
willUnmount() {
c.render();
}
}
class A extends Component {
static template = xml`<B t-key="key" fromA="state"/>`;
static components = { B };
state = "a";
key = 1;
setup() {
a = this;
}
}
const parent = new A();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
a.state = "A";
a.key = 2;
await a.render();
// this nextTick is critical, otherwise jest may silently swallow errors
await nextTick();
expect(fixture.innerHTML).toBe("<div>A</div>");
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
+2 -1
View File
@@ -265,7 +265,8 @@ describe("class and style attributes with t-component", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
});
});
+11 -5
View File
@@ -95,7 +95,9 @@ describe("component error handling (catchError)", () => {
try {
await super.render();
} catch (e) {
expect(e.message).toBe("Cannot read property 'this' of undefined");
expect(e.message).toMatch(
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/
);
}
}
}
@@ -359,7 +361,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -469,7 +472,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -495,7 +499,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
@@ -518,7 +523,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'y' of undefined");
const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error.message).toMatch(regexp);
});
test("simple catchError", async () => {
+67
View File
@@ -1303,4 +1303,71 @@ describe("t-slot directive", () => {
document.querySelector("button").click();
await nextTick();
});
test("t-slot in recursive templates", async () => {
QWeb.registerTemplate(
"_test_recursive_template",
`
<Wrapper>
<t t-esc="name" />
<t t-foreach="items" t-as="item">
<t t-if="!item.children.length">
<t t-esc="item.name" />
</t>
<t t-else="" t-call="_test_recursive_template">
<t t-set="name" t-value="item.name" />
<t t-set="items" t-value="item.children" />
</t>
</t>
</Wrapper>`
);
class Wrapper extends Component {
static template = xml`
<wrapper>
<t t-slot="default"/>
</wrapper>`;
}
class Parent extends Component {
static template = "_test_recursive_template";
static components = { Wrapper };
name = "foo";
items = [
{
name: "foo-0",
children: [
{ name: "foo-00", children: [] },
{
name: "foo-01",
children: [
{ name: "foo-010", children: [] },
{ name: "foo-011", children: [] },
{
name: "foo-012",
children: [
{ name: "foo-0120", children: [] },
{ name: "foo-0121", children: [] },
{ name: "foo-0122", children: [] },
],
},
],
},
{ name: "foo-02", children: [] },
],
},
{ name: "foo-1", children: [] },
{ name: "foo-2", children: [] },
];
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe(
"<wrapper>foo<wrapper>foo-0foo-00<wrapper>foo-01foo-010foo-011<wrapper>foo-012foo-0120foo-0121foo-0122</wrapper></wrapper>foo-02</wrapper>foo-1foo-2</wrapper>"
);
});
});
-6
View File
@@ -75,8 +75,6 @@ describe("hooks", () => {
}
const component = new MyComponent();
await component.mount(fixture);
expect(component).not.toHaveProperty("mounted");
expect(component).not.toHaveProperty("willUnmount");
expect(fixture.innerHTML).toBe("<div>hey</div>");
expect(steps).toEqual(["mounted"]);
component.unmount();
@@ -383,8 +381,6 @@ describe("hooks", () => {
const component = new MyComponent();
await component.mount(fixture);
expect(component).not.toHaveProperty("patched");
expect(component).not.toHaveProperty("willPatch");
expect(steps).toEqual([]);
expect(fixture.innerHTML).toBe("<div>hey</div>");
@@ -630,8 +626,6 @@ describe("hooks", () => {
const app = new App();
await app.mount(fixture);
expect(app).not.toHaveProperty("willStart");
expect(app).not.toHaveProperty("willUpdateProps");
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
// NOTE: 'on2ndStart' appears first in the list even though
+2 -1
View File
@@ -546,7 +546,8 @@ describe("Portal: Basic use and DOM placement", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'crash' of undefined");
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
});
test("portal manual unmount", async () => {
+58 -22
View File
@@ -429,7 +429,8 @@ exports[`foreach does not pollute the rendering context 1`] = `
let vn1 = h('div', p1, c1);
let _2 = [1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -464,7 +465,8 @@ exports[`foreach iterate on items (on a element node) 1`] = `
let vn1 = h('div', p1, c1);
let _2 = [1,2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -502,7 +504,8 @@ exports[`foreach iterate on items 1`] = `
let vn1 = h('div', p1, c1);
let _2 = [3,2,1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -549,7 +552,8 @@ exports[`foreach iterate, dict param 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['value'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -596,7 +600,8 @@ exports[`foreach iterate, position 1`] = `
let vn1 = h('div', p1, c1);
let _2 = Array(5);
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -641,7 +646,8 @@ exports[`foreach t-call with body in t-foreach in t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['numbers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -658,7 +664,8 @@ exports[`foreach t-call with body in t-foreach in t-foreach 1`] = `
let key1 = i1;
let _6 = scope['letters'];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -729,7 +736,8 @@ exports[`foreach t-call without body in t-foreach in t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['numbers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -746,7 +754,8 @@ exports[`foreach t-call without body in t-foreach in t-foreach 1`] = `
let key1 = i1;
let _6 = scope['letters'];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -811,7 +820,8 @@ exports[`foreach t-foreach in t-forach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['numbers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -828,7 +838,8 @@ exports[`foreach t-foreach in t-forach 1`] = `
let key1 = i1;
let _6 = scope['letters'];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -871,7 +882,8 @@ exports[`foreach warn if no key in some case 1`] = `
let vn1 = h('div', p1, c1);
let _2 = [1,2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -940,7 +952,8 @@ exports[`misc global 1`] = `
let vn1 = h('div', p1, c1);
let _2 = [4,5,6];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -1666,7 +1679,8 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
}
let _6 = scope['node'].children||[];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -1746,7 +1760,8 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
}
let _6 = scope['node'].children||[];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -1832,7 +1847,8 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
}
let _6 = scope['node'].children||[];
if (!_6) { throw new Error('QWeb error: Invalid loop expression')}
let _7 = _8 = _6;
let _7 = _6;
let _8 = _6;
if (!(_6 instanceof Array)) {
_7 = Object.keys(_6);
_8 = Object.values(_6);
@@ -1998,7 +2014,8 @@ exports[`t-call (template calling t-call with t-set inside and outside 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['list'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -2775,7 +2792,8 @@ exports[`t-key t-key directive in a list 1`] = `
let vn1 = h('ul', p1, c1);
let _2 = scope['beers'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -2879,7 +2897,8 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = `
let vn1 = h('ul', p1, c1);
let _2 = ['someval'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -3161,7 +3180,8 @@ exports[`t-on t-on with prevent modifier in t-foreach 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['projects'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -3432,7 +3452,8 @@ exports[`t-ref refs in a loop 1`] = `
let vn1 = h('div', p1, c1);
let _2 = scope['items'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -3676,7 +3697,8 @@ exports[`t-set t-set should reuse variable if possible 1`] = `
scope.v = 1;
let _2 = scope['list'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
@@ -3943,6 +3965,20 @@ exports[`t-set value priority 1`] = `
}"
`;
exports[`translation support can add additional attributes to the list of translatable attributes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let h = this.h;
let _1 = 'word';
let _2 = 'mot';
let c3 = [], p3 = {key:3,attrs:{tomato: _1,potato: _2}};
let vn3 = h('div', p3, c3);
c3.push({text: \`text\`});
return vn3;
}"
`;
exports[`translation support can translate node content 1`] = `
"function anonymous(context, extra
) {
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`memory t-foreach does not leak stuff in global scope 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('p', p1, c1);
let _2 = [3,2,1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _2;
let _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.item_first = i1 === 0
scope.item_last = i1 === _length3 - 1
scope.item_index = i1
scope.item = _3[i1]
scope.item_value = _4[i1]
let key1 = i1;
let _6 = scope['item'];
if (_6 != null) {
c1.push({text: _6});
}
}
scope = _origScope5;
return vn1;
}"
`;
@@ -27,3 +27,31 @@ exports[`qweb t-att t-att-class with multiple classes 2`] = `
return vn4;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
+15 -4
View File
@@ -1,4 +1,5 @@
import { QWeb } from "../../src/qweb/index";
import { config } from "../../src/index";
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
import { patch } from "../../src/vdom";
@@ -13,6 +14,7 @@ let qweb: QWeb;
beforeEach(() => {
QWeb.TEMPLATES = {};
QWeb.nextId = 1;
qweb = new QWeb();
});
@@ -1862,8 +1864,7 @@ describe("t-ref", () => {
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const templates = `
<?xml version="1.0" encoding="UTF-8"?>
const templates = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
@@ -1872,8 +1873,7 @@ describe("loading templates", () => {
});
test("can load a few templates from a xml string", () => {
const data = `
<?xml version="1.0" encoding="UTF-8"?>
const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="items"><li>ok</li><li>foo</li></t>
@@ -2213,6 +2213,17 @@ describe("translation support", () => {
);
});
test("can add additional attributes to the list of translatable attributes", () => {
const translations = {
word: "mot",
};
const translateFn = (expr) => translations[expr] || expr;
const qweb = new QWeb({ translateFn });
config.translatableAttributes.push("potato");
qweb.addTemplate("test", `<div tomato="word" potato="word">text</div>`);
expect(renderToString(qweb, "test")).toBe('<div tomato="word" potato="mot">text</div>');
});
test("translation is done on the trimmed text, with extra spaces readded after", () => {
const translations = {
word: "mot",
+14
View File
@@ -0,0 +1,14 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
describe("memory", () => {
test("t-foreach does not leak stuff in global scope", () => {
let qweb = new QWeb();
const initialNumberOfGlobals = Object.keys(window).length;
qweb.addTemplate("test", `<p><t t-foreach="[3, 2, 1]" t-as="item"><t t-esc="item"/></t></p>`);
const result = renderToString(qweb, "test");
const expected = `<p>321</p>`;
expect(result).toBe(expected);
expect(Object.keys(window).length).toBe(initialNumberOfGlobals);
});
});
+9
View File
@@ -24,4 +24,13 @@ describe("qweb t-att", () => {
'<div class="a b c"></div>'
);
});
test("t-att-class with multiple classes, some of which are duplicate", () => {
expect(render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(
render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: false })
).toBe('<div class="a b d"></div>');
});
});
+1 -1
View File
@@ -80,7 +80,7 @@ describe("Link component", () => {
await app.mount(fixture);
expect(window.location.pathname).toBe("/users");
var evt = new MouseEvent("click", {
var evt = new MouseEvent("contextmenu", {
button: 1,
});
+2 -2
View File
@@ -13,8 +13,8 @@
// "compileOnSave": false,                   // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
"compilerOptions": {
                                                            // Main options
"target": "esnext",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "esnext",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
"target": "es2017",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "es6",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
// "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation.
// "allowJs": false,                 // Allow javascript files to be compiled.
// "checkJs": false,                 // Report errors in .js files.