mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[DOC] update quick_start and how to test pages
This commit is contained in:
committed by
Samuel Degueldre
parent
3f09a953b0
commit
f014b1a33e
+11
-46
@@ -30,27 +30,21 @@ To help with this, it is useful to have a `helper.js` file that contains some
|
|||||||
common utility functions:
|
common utility functions:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
let lastFixture = null;
|
||||||
|
|
||||||
export function makeTestFixture() {
|
export function makeTestFixture() {
|
||||||
let fixture = document.createElement("div");
|
let fixture = document.createElement("div");
|
||||||
document.body.appendChild(fixture);
|
document.body.appendChild(fixture);
|
||||||
|
if (lastFixture) {
|
||||||
|
lastFixture.remove();
|
||||||
|
}
|
||||||
|
lastFixture = fixture;
|
||||||
return fixture;
|
return fixture;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextTick() {
|
export async function nextTick() {
|
||||||
let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
return new Promise(function(resolve) {
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
setTimeout(() => requestAnimationFrame(() => resolve()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeTestEnv() {
|
|
||||||
// application specific. It needs a way to load actual templates
|
|
||||||
const templates = ...;
|
|
||||||
|
|
||||||
return {
|
|
||||||
qweb: new QWeb(templates),
|
|
||||||
..., // each service can be mocked here
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -59,7 +53,7 @@ With such a file, a typical test suite for Jest will look like this:
|
|||||||
```js
|
```js
|
||||||
// in SomeComponent.test.js
|
// in SomeComponent.test.js
|
||||||
import { SomeComponent } from "../../src/ui/SomeComponent";
|
import { SomeComponent } from "../../src/ui/SomeComponent";
|
||||||
import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
|
import { nextTick, makeTestFixture } from '../helpers';
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -70,9 +64,6 @@ let env: Env;
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = makeTestFixture();
|
fixture = makeTestFixture();
|
||||||
env = makeTestEnv();
|
|
||||||
// we set here the default environment for each component created in the test
|
|
||||||
Component.env = env;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -85,7 +76,7 @@ afterEach(() => {
|
|||||||
describe("SomeComponent", () => {
|
describe("SomeComponent", () => {
|
||||||
test("component behaves as expected", async () => {
|
test("component behaves as expected", async () => {
|
||||||
const props = {...}; // depends on the component
|
const props = {...}; // depends on the component
|
||||||
const comp = await mount(SomeComponent, { target: fixture, props });
|
const comp = await mount(SomeComponent, fixture, { props });
|
||||||
|
|
||||||
// do some assertions
|
// do some assertions
|
||||||
expect(...).toBe(...);
|
expect(...).toBe(...);
|
||||||
@@ -102,29 +93,3 @@ describe("SomeComponent", () => {
|
|||||||
Note that Owl does wait for the next animation frame to actually update the DOM.
|
Note that Owl does wait for the next animation frame to actually update the DOM.
|
||||||
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
||||||
make sure that the DOM is up-to-date.
|
make sure that the DOM is up-to-date.
|
||||||
|
|
||||||
It is sometimes useful to wait until Owl is completely done updating components
|
|
||||||
(in particular, if we have a highly concurrent user interface). This next
|
|
||||||
helper simply polls every 20ms the internal Owl task queue and returns a promise
|
|
||||||
which resolves when it is empty:
|
|
||||||
|
|
||||||
```js
|
|
||||||
function afterUpdates() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timer = setTimeout(poll, 20);
|
|
||||||
let counter = 0;
|
|
||||||
function poll() {
|
|
||||||
counter++;
|
|
||||||
if (owl.Component.scheduler.tasks.length) {
|
|
||||||
if (counter > 10) {
|
|
||||||
reject(new Error("timeout"));
|
|
||||||
} else {
|
|
||||||
timer = setTimeout(poll);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
+38
-47
@@ -36,6 +36,8 @@ hello_owl/
|
|||||||
The file `owl.js` can be downloaded from the last release published at
|
The file `owl.js` can be downloaded from the last release published at
|
||||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
|
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
|
||||||
is a single javascript file which export all Owl into the global `owl` object.
|
is a single javascript file which export all Owl into the global `owl` object.
|
||||||
|
Note that there are multiple files, and in this case, we need one of the two
|
||||||
|
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||||
|
|
||||||
Now, `index.html` should contain the following:
|
Now, `index.html` should contain the following:
|
||||||
|
|
||||||
@@ -45,30 +47,24 @@ Now, `index.html` should contain the following:
|
|||||||
<head>
|
<head>
|
||||||
<title>Hello Owl</title>
|
<title>Hello Owl</title>
|
||||||
<script src="owl.js"></script>
|
<script src="owl.js"></script>
|
||||||
<script src="app.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
And `app.js` should look like this:
|
And `app.js` should look like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { Component, mount } = owl;
|
const { Component, mount, xml } = owl;
|
||||||
const { xml } = owl.tags;
|
|
||||||
const { whenReady } = owl.utils;
|
|
||||||
|
|
||||||
// Owl Components
|
// Owl Components
|
||||||
class App extends Component {
|
class Root extends Component {
|
||||||
static template = xml`<div>Hello Owl</div>`;
|
static template = xml`<div>Hello Owl</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup code
|
mount(Root, document.body);
|
||||||
function setup() {
|
|
||||||
mount(App, target: { document.body })
|
|
||||||
}
|
|
||||||
|
|
||||||
whenReady(setup);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, simply loading this html file in a browser should display a welcome message.
|
Now, simply loading this html file in a browser should display a welcome message.
|
||||||
@@ -93,14 +89,16 @@ Let us start a new project with the following file structure:
|
|||||||
```
|
```
|
||||||
hello_owl/
|
hello_owl/
|
||||||
src/
|
src/
|
||||||
app.js
|
|
||||||
index.html
|
index.html
|
||||||
main.js
|
main.js
|
||||||
owl.js
|
owl.js
|
||||||
|
root.js
|
||||||
```
|
```
|
||||||
|
|
||||||
As previously, the file `owl.js` can be downloaded from the last release published at
|
As previously, the file `owl.js` can be downloaded from the last release published at
|
||||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
|
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
|
||||||
|
Note that there are multiple files, and in this case, we need one of the two
|
||||||
|
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||||
|
|
||||||
Now, `index.html` should contain the following:
|
Now, `index.html` should contain the following:
|
||||||
|
|
||||||
@@ -110,37 +108,33 @@ Now, `index.html` should contain the following:
|
|||||||
<head>
|
<head>
|
||||||
<title>Hello Owl</title>
|
<title>Hello Owl</title>
|
||||||
<script src="owl.js"></script>
|
<script src="owl.js"></script>
|
||||||
<script src="main.js" type="module"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body>
|
||||||
|
<script src="main.js" type="module"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
Not that the `main.js` script tag has the `type="module"` attribute. This means
|
Not that the `main.js` script tag has the `type="module"` attribute. This means
|
||||||
that the browser will parse the script as a module, and load all its dependencies.
|
that the browser will parse the script as a module, and load all its dependencies.
|
||||||
|
|
||||||
Here is the content of `app.js` and `main.js`:
|
Here is the content of `root.js` and `main.js`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// app.js ----------------------------------------------------------------------
|
// root.js ----------------------------------------------------------------------
|
||||||
const { Component, mount } = owl;
|
const { Component, mount, xml } = owl;
|
||||||
const { xml } = owl.tags;
|
|
||||||
|
|
||||||
export class App extends Component {
|
export class Root extends Component {
|
||||||
static template = xml`<div>Hello Owl</div>`;
|
static template = xml`<div>Hello Owl</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// main.js ---------------------------------------------------------------------
|
// main.js ---------------------------------------------------------------------
|
||||||
import { App } from "./app.js";
|
import { Root } from "./root.js";
|
||||||
|
|
||||||
function setup() {
|
mount(Root, document.body);
|
||||||
mount(App, { target: document.body });
|
|
||||||
}
|
|
||||||
|
|
||||||
owl.utils.whenReady(setup);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `main.js` file import the `app.js` file. Note that the import statement has
|
The `main.js` file imports the `root.js` file. Note that the import statement has
|
||||||
a `.js` suffix, which is important. Most text editor can understand this syntax
|
a `.js` suffix, which is important. Most text editor can understand this syntax
|
||||||
and will provide autocompletion.
|
and will provide autocompletion.
|
||||||
|
|
||||||
@@ -193,11 +187,11 @@ hello_owl/
|
|||||||
index.html
|
index.html
|
||||||
src/
|
src/
|
||||||
components/
|
components/
|
||||||
App.js
|
Root.js
|
||||||
main.js
|
main.js
|
||||||
tests/
|
tests/
|
||||||
components/
|
components/
|
||||||
App.test.js
|
Root.test.js
|
||||||
helpers.js
|
helpers.js
|
||||||
.gitignore
|
.gitignore
|
||||||
package.json
|
package.json
|
||||||
@@ -224,13 +218,15 @@ Note that there are no `<script>` tag here. They will be injected by webpack.
|
|||||||
Now, let's have a look at the javascript files:
|
Now, let's have a look at the javascript files:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// src/components/App.js -------------------------------------------------------
|
// src/components/Root.js -------------------------------------------------------
|
||||||
import { Component, tags, useState } from "@odoo/owl";
|
import { Component, xml, useState } from "@odoo/owl";
|
||||||
|
|
||||||
const { xml } = tags;
|
export class Root extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-click="update">
|
||||||
|
Hello <t t-esc="state.text"/>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
export class App extends Component {
|
|
||||||
static template = xml`<div t-on-click="update">Hello <t t-esc="state.text"/></div>`;
|
|
||||||
state = useState({ text: "Owl" });
|
state = useState({ text: "Owl" });
|
||||||
update() {
|
update() {
|
||||||
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
|
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
|
||||||
@@ -239,16 +235,12 @@ export class App extends Component {
|
|||||||
|
|
||||||
// src/main.js -----------------------------------------------------------------
|
// src/main.js -----------------------------------------------------------------
|
||||||
import { utils, mount } from "@odoo/owl";
|
import { utils, mount } from "@odoo/owl";
|
||||||
import { App } from "./components/App";
|
import { Root } from "./components/Root";
|
||||||
|
|
||||||
function setup() {
|
mount(Root, document.body);
|
||||||
mount(App, { target: document.body });
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.whenReady(setup);
|
// tests/components/Root.test.js ------------------------------------------------
|
||||||
|
import { Root } from "../../src/components/Root";
|
||||||
// tests/components/App.test.js ------------------------------------------------
|
|
||||||
import { App } from "../../src/components/App";
|
|
||||||
import { makeTestFixture, nextTick, click } from "../helpers";
|
import { makeTestFixture, nextTick, click } from "../helpers";
|
||||||
import { mount } from "@odoo/owl";
|
import { mount } from "@odoo/owl";
|
||||||
|
|
||||||
@@ -262,9 +254,9 @@ afterEach(() => {
|
|||||||
fixture.remove();
|
fixture.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("App", () => {
|
describe("Root", () => {
|
||||||
test("Works as expected...", async () => {
|
test("Works as expected...", async () => {
|
||||||
await mount(App, { target: fixture });
|
await mount(Root, fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
|
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
|
||||||
|
|
||||||
click(fixture, "div");
|
click(fixture, "div");
|
||||||
@@ -278,9 +270,8 @@ import { Component } from "@odoo/owl";
|
|||||||
import "regenerator-runtime/runtime";
|
import "regenerator-runtime/runtime";
|
||||||
|
|
||||||
export async function nextTick() {
|
export async function nextTick() {
|
||||||
return new Promise(function (resolve) {
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeTestFixture() {
|
export function makeTestFixture() {
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ deleteTask(task) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
||||||
suffix that make sure the function callback is bound to the component.
|
suffix that makes sure the function callback is bound to the component.
|
||||||
|
|
||||||
## 10. Using a store
|
## 10. Using a store
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user