Compare commits

..

20 Commits

Author SHA1 Message Date
Simon Genin (ges) 5efc9ad462 [FIX] remove indeterminism in animation tests
Animations have quite a few intricate calls to request next animation
frames. On quick devices, the helper method nextFrame made of 2
request of animation frames would not be enough and race condition
problems would rise.

The fix is simply to ask the nextFrame helper to do a third request
of animation frame in the animation tests, making the test deterministic
on a very fast device. Hopefully, this should be enough.
2021-10-04 11:21:55 +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
Géry Debongnie 8083678f03 [REL] v1.4.2
# 1.4.2

- qweb: properly handle inline expressions with lists such as '[a,b,c]'
2021-07-07 14:56:46 +02:00
Géry Debongnie 61c2ec5d83 [FIX] qweb: properly handle lists in inline expressions
Before this commit, Owl inline expressions with a list with multiple
elements such as [a,b,c] was transformed into

[scope['a'], b: scope['b'], scope['c']]

instead of

[scope['a'], scope['b'], scope['c']]

This is due to a previous commit adding support for short object
descriptions such as {a,b}.

To fix this means that we have to keep track of the current group type
for the expression, which is done by using a stack.
2021-07-07 14:50:27 +02:00
Géry Debongnie e579a993fd [REL] v1.4.1
# 1.4.1

This release brings in two small fixes:

- vdom: a performance improvement to reduce number of calls to classList.remove
- slots: fix subtle issue with wrong context used in event handlers when
  multiple slots are involved
2021-07-07 11:10:05 +02:00
Géry Debongnie ea3c6f7bf0 [FIX] slots: make sure handlers are properly bound to component
In some situations (a slot inside a slot), the combine method was
wrongly copying all properties of the scope into the context, which
caused the event handling system to wrongly use a subobject as component
(since it detects the fact that __owl__ is a own property_).
Consequently, we could have very subtle issue with some properties being
shadowed by a sub object.
2021-07-07 11:04:25 +02:00
Pierre Paridans 9cfafc30b5 [IMP] vdom: performance improvement
Port from original snabbdom project: snabbdom/snabbdom#634

The issue is that before this commit, the removeClass method was
sometimes called even if it is not useful. See this comment for more
detail: https://github.com/snabbdom/snabbdom/issues/633#issue-618706258
2021-07-06 14:14:40 +02:00
Aaron Bohy ad42c583c6 [FIX] package.json: bump node version to v12
Node 10.x is no longer maintained, and since a recent commit [1],
a test fails with that version.

[1] 88fd1cf483
2021-07-06 13:30:17 +02:00
22 changed files with 293 additions and 49 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [10.x, 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.0](https://github.com/odoo/owl/releases/tag/v1.4.0)
- [owl-1.4.5](https://github.com/odoo/owl/releases/tag/v1.4.5)
## 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
+9 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.4.0",
"version": "1.4.5",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
@@ -10,7 +10,7 @@
"dist"
],
"engines": {
"node": ">=10.15.3"
"node": ">=12.18.3"
},
"scripts": {
"build:bundle": "rollup -c",
@@ -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.0
- Current version: 1.4.5
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+5 -1
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() {
+18 -1
View File
@@ -257,16 +257,33 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
const tokens = tokenize(expr);
let i = 0;
let stack = []; // to track last opening [ or {
while (i < tokens.length) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let groupType = stack[stack.length - 1];
switch (token.type) {
case "LEFT_BRACE":
case "LEFT_BRACKET":
stack.push(token.type);
break;
case "RIGHT_BRACE":
case "RIGHT_BRACKET":
stack.pop();
}
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
if (prevToken) {
// normalize missing tokens: {a} should be equivalent to {a:a}
if (isLeftSeparator(prevToken) && isRightSeparator(nextToken)) {
if (
groupType === "LEFT_BRACE" &&
isLeftSeparator(prevToken) &&
isRightSeparator(nextToken)
) {
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
nextToken = tokens[i + 1];
}
+34 -8
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,20 +127,35 @@ 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);
for (let k in scope) {
clone[k] = scope[k];
let clone = context;
const scopeStack = [];
while (!isComponent(scope)) {
scopeStack.push(scope);
scope = scope.__proto__;
}
while (scopeStack.length) {
let scope = scopeStack.pop();
clone = Object.create(clone);
Object.assign(clone, scope);
}
return clone;
},
@@ -314,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) {
@@ -542,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);
@@ -578,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);
+2 -1
View File
@@ -231,7 +231,8 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element;
for (name in oldClass) {
if (name && !klass[name]) {
if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
// was `true` and now not provided
elm.classList.remove(name);
}
}
+5 -5
View File
@@ -420,29 +420,29 @@ describe("animations", () => {
widget.state.flag = true;
await nextFrame();
await nextFrame(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = false;
await nextFrame();
await nextFrame(3);
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
widget.state.flag = true;
await nextFrame();
await nextFrame(3);
expect(fixture.innerHTML).toBe(
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
widget.state.flag = false;
await nextFrame();
await nextFrame(3);
widget.state.flag = true;
await nextFrame();
await nextFrame(3);
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
+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 () => {
+113
View File
@@ -1257,4 +1257,117 @@ describe("t-slot directive", () => {
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
);
});
test("t-slot scope context", async () => {
expect.assertions(1);
class Wrapper extends Component {
static template = xml`<t t-slot="default"/>`;
}
let dialog;
class Dialog extends Component {
static template = xml`
<Wrapper>
<div t-on-click="onClick">
<t t-slot="default" />
</div>
</Wrapper>
`;
static components = { Wrapper };
setup() {
dialog = this;
}
onClick(ev) {
// we do not use expect(this).toBe(dialog) here because if it fails, it
// may blow up jest because it then tries to compute a diff, which is
// infinite if there is a cycle
expect(this === dialog).toBe(true);
}
}
class Parent extends Component {
static template = xml`
<Dialog>
<button>The Button</button>
</Dialog>`;
static components = { Dialog };
}
await mount(Parent, { target: fixture });
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>"
);
});
});
+4 -3
View File
@@ -41,9 +41,10 @@ export async function nextTick(): Promise<void> {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
}
export async function nextFrame(): Promise<void> {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
export async function nextFrame(numberOfAnimationFrames: number = 2): Promise<void> {
for (let i = 0; i < numberOfAnimationFrames; i++) {
await new Promise((resolve) => scheduler.requestAnimationFrame(resolve));
}
}
export function makeTestFixture() {
-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 () => {
@@ -3943,6 +3943,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
) {
@@ -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",
+11
View File
@@ -207,6 +207,17 @@ describe("expression evaluation", () => {
expect(compileExpr("{a,b:3,c}", {})).toBe("{a:scope['a'],b:3,c:scope['c']}");
});
test("works with short object description and lists ", () => {
expect(compileExpr("[a, b]", {})).toBe("[scope['a'],scope['b']]");
expect(compileExpr("[a, b, c]", {})).toBe("[scope['a'],scope['b'],scope['c']]");
expect(compileExpr("[a, {b, c},d]", {})).toBe(
"[scope['a'],{b:scope['b'],c:scope['c']},scope['d']]"
);
expect(compileExpr("{a:[b, {c, d: e}]}", {})).toBe(
"{a:[scope['b'],{c:scope['c'],d:scope['e']}]}"
);
});
test("template strings", () => {
expect(compileExpr("`hey`", {})).toBe("`hey`");
expect(compileExpr("`hey ${you}`", {})).toBe("`hey ${scope['you']}`");
+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,
});