mirror of
https://github.com/odoo/documentation.git
synced 2025-12-22 12:29:10 +07:00
[IMP] web: HOOT - documentation
X-original-commit: eb756332c9
Part-of: odoo/documentation#14926
Signed-off-by: Julien Mougenot (jum) <jum@odoo.com>
This commit is contained in:
@@ -21,3 +21,4 @@ Web framework
|
|||||||
frontend/mobile
|
frontend/mobile
|
||||||
frontend/qweb
|
frontend/qweb
|
||||||
frontend/odoo_editor
|
frontend/odoo_editor
|
||||||
|
frontend/unit_testing
|
||||||
|
|||||||
90
content/developer/reference/frontend/unit_testing.rst
Normal file
90
content/developer/reference/frontend/unit_testing.rst
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
:show-content:
|
||||||
|
:show-toc:
|
||||||
|
|
||||||
|
=======================
|
||||||
|
JavaScript Unit Testing
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Writing unit tests is as important as writing the code itself: it helps to
|
||||||
|
ensure that the code is written according to a given specification and that it
|
||||||
|
remains correct as it evolves.
|
||||||
|
|
||||||
|
Testing Framework
|
||||||
|
=================
|
||||||
|
|
||||||
|
Testing the code starts with a testing framework. The framework provides a level
|
||||||
|
of abstraction that makes it possible to write tests in an easy and efficient way.
|
||||||
|
It also provides a set of tools to run the tests, make assertions and report the
|
||||||
|
results.
|
||||||
|
|
||||||
|
Odoo developers use a home-grown testing framework called :abbr:`HOOT (Hierarchically Organized
|
||||||
|
Odoo Tests)`. The main reason for using a custom framework is that it allows us to extend it based
|
||||||
|
on our needs (tags system, mocking of global objects, etc.).
|
||||||
|
|
||||||
|
On top of that framework we have built a set of tools to help us write tests for the web client
|
||||||
|
(`web_test_helpers`), and a mock server to simulate the server side (`mock_server`).
|
||||||
|
|
||||||
|
You can find links to the reference of each of these parts below, as well as a section filled with
|
||||||
|
examples and best practices for writing tests.
|
||||||
|
|
||||||
|
Setup
|
||||||
|
=====
|
||||||
|
|
||||||
|
Before learning how to write tests, it is good to start with the basics. The following steps
|
||||||
|
will ensure that your test files are properly picked up by the test runner.
|
||||||
|
|
||||||
|
Note that in existing addons, most of these steps can be skipped since the proper
|
||||||
|
folder structure and asset bundles are probably set up.
|
||||||
|
|
||||||
|
#. Writing files in the right **place**:
|
||||||
|
|
||||||
|
All JavaScript test files should be put under the `/static/tests` folder of the
|
||||||
|
related addon (e.g. :file:`/web/static/tests/env.test.js`).
|
||||||
|
|
||||||
|
#. Using the right **name**:
|
||||||
|
|
||||||
|
Test files must end with :file:`.test.js`. This is not only a convention, but a requirement
|
||||||
|
for test files to be picked up by the runner. All other JavaScript files will be
|
||||||
|
interpreted either as production code (i.e. the code to be tested), or as test
|
||||||
|
helper files (such as `web_test_helpers <{GITHUB_PATH}/addons/web/static/tests/web_test_helpers.js>`_).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is to be noted that there is an exception for :file:`.hoot.js` files, which are not
|
||||||
|
considered as test files, but as global modules for the whole test run, while other
|
||||||
|
JavaScript modules are re-created for each test suite. Since the same instance of
|
||||||
|
these modules will be running for the whole test run, they follow strict constraints,
|
||||||
|
such as restricted imports, or advanced memory management techniques to
|
||||||
|
ensure no side-effects are affecting tests.
|
||||||
|
|
||||||
|
#. Calling the files in the right **bundle**:
|
||||||
|
|
||||||
|
Test files, added in the right folder, must be included in the `web.assets_unit_tests`
|
||||||
|
bundle. For ease of use, this can be done with glob syntax to import all test
|
||||||
|
and test helper files:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# Unit test files
|
||||||
|
'web.assets_unit_tests': [
|
||||||
|
'my_addon/static/tests/**/*',
|
||||||
|
],
|
||||||
|
|
||||||
|
#. Heading to the right **URL**:
|
||||||
|
|
||||||
|
To run tests, you can then go to the `/web/tests` URL.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
This page can be accessed through :icon:`fa-bug` :menuselection:`Debug menu --> Run Unit Tests`.
|
||||||
|
|
||||||
|
Writing tests
|
||||||
|
=============
|
||||||
|
|
||||||
|
After creating and including test files, it is time to write tests. You may refer
|
||||||
|
to the following documentation sections to learn about the testing framework.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:titlesonly:
|
||||||
|
|
||||||
|
unit_testing/hoot
|
||||||
|
unit_testing/web_helpers
|
||||||
|
unit_testing/mock_server
|
||||||
1662
content/developer/reference/frontend/unit_testing/hoot.rst
Normal file
1662
content/developer/reference/frontend/unit_testing/hoot.rst
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,383 @@
|
|||||||
|
===========
|
||||||
|
Mock server
|
||||||
|
===========
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
=========
|
||||||
|
|
||||||
|
Test cases can range in complexity; from testing the return value of a simple helper
|
||||||
|
function, to rendering an entire webclient to simulate interactions across several
|
||||||
|
components/services.
|
||||||
|
|
||||||
|
In the latter cases, many interactions will trigger server requests, without
|
||||||
|
which the components or features will stop functioning properly. However, it is
|
||||||
|
important that these requests *do not* land on the actual server, as they could
|
||||||
|
affect the database, which is definitely not something a test should be doing.
|
||||||
|
|
||||||
|
To overcome this, each request should be intercepted and replaced by a function
|
||||||
|
emulating actual server responses with test (i.e. fake) data.
|
||||||
|
|
||||||
|
Since some of these requests are very common (e.g. ORM calls, such as `web_search_read`
|
||||||
|
or `web_save`, or other methods such as `get_views`), a mock server has been
|
||||||
|
implemented by default for every test that spawns an :abbr:`env (Odoo environment)` [#]_.
|
||||||
|
|
||||||
|
These mock servers act independently for each test, can be configured separately,
|
||||||
|
and provide out of the box helpers for the most used routes in Odoo.
|
||||||
|
|
||||||
|
.. [#] A mock server is needed as soon as an environment is spawned because some :doc:`services <../services>`
|
||||||
|
do send server requests as soon as they start.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
A mock server is actually quite simple in itself: it is an object containing a *collection*
|
||||||
|
of all defined mock models, and a *mapping* between routes and callbacks returning
|
||||||
|
the test data.
|
||||||
|
|
||||||
|
The mock models themselves hold most of the CRUD logic, as well as the data used
|
||||||
|
to simulate server records.
|
||||||
|
|
||||||
|
Once a mock server starts, it hijacks *all* server requests, and for each of them
|
||||||
|
it will check in its *mapping* whether one of its registered routes matches
|
||||||
|
the requested URL. The most notable example of its pre-defined routes is
|
||||||
|
`/web/dataset/call_kw`, which is responsible for calling an ORM method on the
|
||||||
|
appropriate mock model.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Like most test helpers not provided by Hoot, mock server-related helpers and
|
||||||
|
classes can be found in the `"@web/../tests/web_test_helpers"` module.
|
||||||
|
|
||||||
|
.. _mock-server/configuration:
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
By default, a mock server is *"empty"*, meaning that it has no defined mock model.
|
||||||
|
|
||||||
|
This does not mean that it is useless though, as it will already handle a few pre-defined
|
||||||
|
routes, such as the ones responsible for fetching `menus` and `translations`, which
|
||||||
|
are spawned by :doc:`services <../services>` as soon as an `env` is spawned.
|
||||||
|
|
||||||
|
But this means that ORM methods will fail, as the model that they target has not
|
||||||
|
been defined yet.
|
||||||
|
|
||||||
|
To create and define a mock model, you need 2 things:
|
||||||
|
|
||||||
|
- a `class` extending the `models.Model` class;
|
||||||
|
|
||||||
|
- special keys prefixed with a `_` act as metadata holders, like in Python
|
||||||
|
(e.g. `_name`, `_order`, `_description`, etc.) [#]_ [#]_;
|
||||||
|
|
||||||
|
- `_records` holds the list of objects representing fake record data;
|
||||||
|
|
||||||
|
- `_views` can be a mapping of view types and XML arches;
|
||||||
|
|
||||||
|
- other `public class fields <public-class-fields_>`_
|
||||||
|
will be interpreted as fields (by calling the appropriate method from `fields`);
|
||||||
|
|
||||||
|
- model-specific methods (such as `has_group` for `"res.users"`) can also be
|
||||||
|
defined here.
|
||||||
|
|
||||||
|
- calling `defineModels` with the class defined above.
|
||||||
|
|
||||||
|
.. [#] Only a subset of these special keys will have an actual effect. For example,
|
||||||
|
`_inherit` will not work as intended, prefer standard class extension.
|
||||||
|
|
||||||
|
.. [#] These can be altered by each test without thinking about cleaning up: any change
|
||||||
|
performed on a special key will be reverted at the end of a test.
|
||||||
|
|
||||||
|
Here is a basic example of a simple, fake, `"res.partner"` model:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
import { defineModels, fields, models } from "@web/../tests/web_test_helpers";
|
||||||
|
|
||||||
|
class ResPartner extends models.Model {
|
||||||
|
_name = "res.partner";
|
||||||
|
|
||||||
|
name = fields.Char({ required: true );
|
||||||
|
|
||||||
|
_records = [
|
||||||
|
{ name: "Mitchel Admin" },
|
||||||
|
];
|
||||||
|
|
||||||
|
_views = {
|
||||||
|
form: /* xml */`
|
||||||
|
<form>
|
||||||
|
<field name="name" />
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
list: /* xml */`
|
||||||
|
<list>
|
||||||
|
<field name="display_name" />
|
||||||
|
</list>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
defineModels({ ResPartner });
|
||||||
|
|
||||||
|
This code will make these data available for *all* tests in the current test file.
|
||||||
|
Of course, defining a class and calling `defineModels` can also be done from *within*
|
||||||
|
a given test to limit the scope of that model to the current test.
|
||||||
|
|
||||||
|
Other methods such as `defineMenus`, `defineActions` or `defineParams` can also
|
||||||
|
be used to configure the current mock server. Most of their API is quite straightforward
|
||||||
|
(i.e. they receive JSON-like descriptions of menus, actions, etc.).
|
||||||
|
|
||||||
|
Mock models: requests
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Many test cases only require one or a few mock models to work. But sometimes,
|
||||||
|
it is either too bothersome to implement the mocking logic within a model, or a
|
||||||
|
*route* (i.e. server request URL) is simply not associated to a Python model at all.
|
||||||
|
|
||||||
|
In such cases, the `onRpc` method is to be called, to associate a route or an ORM
|
||||||
|
method to a callback.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Multiple `onRpc` calls can be associated to the same route / ORM method;
|
||||||
|
in which case they will be called sequentially from last to first defined.
|
||||||
|
Returning a *non-null-or-undefined* value will interrupt the current chain,
|
||||||
|
and return that value as final result of the server request.
|
||||||
|
|
||||||
|
It can be used in 4 different ways:
|
||||||
|
|
||||||
|
`onRpc`: with a route (`"/"`)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When the first argument is a `string` starting with a `"/"`, the callback
|
||||||
|
is expected to be a *route* callback, receiving a `Request_`
|
||||||
|
object:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
onRpc("/route/to/test", async (request) => {
|
||||||
|
const { ids } = await request.json();
|
||||||
|
expect.step(ids);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
By default, the return value of these callbacks are wrapped within the `body`
|
||||||
|
of a mock `Response_` object.
|
||||||
|
|
||||||
|
This is fine for most use-cases, but sometimes the callback needs to respond with
|
||||||
|
a `Response_` object with custom `status` or `headers`.
|
||||||
|
|
||||||
|
In such cases, an *optional* dictionary can be passed as a 3rd argument to specify
|
||||||
|
whether the callback is to be considered *"pure"*, meaning that its return value
|
||||||
|
should be returned as-is to the server caller:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
onRpc(
|
||||||
|
"/not/found",
|
||||||
|
() => new Response("{}", { status: 404 }),
|
||||||
|
{ pure: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Using *"pure"* request callbacks can also be used to return anything else than
|
||||||
|
a `Response_` object, in which case the returned value will still be wrapped
|
||||||
|
in the body of a mock `Response_` to comply with the `fetch_` / `XMLHttpRequest_`
|
||||||
|
APIs.
|
||||||
|
|
||||||
|
`onRpc`: with method name(s)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When the first argument is a `string` *NOT* starting with a `"/"` or a list of `strings`,
|
||||||
|
the callback is expected to be an ORM callback, only called when the request's `method`
|
||||||
|
matches the one given as argument.
|
||||||
|
|
||||||
|
The callback will receive an object containing:
|
||||||
|
|
||||||
|
- the *spread* `params` value contained in the request body (typically: `args`,
|
||||||
|
`kwargs`, `model` and `method`);
|
||||||
|
|
||||||
|
- a `parent()` function, which when invoked will call the defined ORM callback *preceding*
|
||||||
|
this one;
|
||||||
|
|
||||||
|
- a `route` key, containing the `pathname` of the request (typically: `/web/dataset/call_kw`);
|
||||||
|
|
||||||
|
- the `request` object.
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
onRpc("web_read", async ({ args, parent }) => {
|
||||||
|
const result = parent();
|
||||||
|
expect.step(args[0]); // Contains the list of IDs
|
||||||
|
result.some_meta_data = { foo: "bar" };
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
`onRpc`: with model name(s) AND method name(s)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When:
|
||||||
|
|
||||||
|
- the first argument is a `string` *NOT* starting with a `"/"` or a list of `strings`;
|
||||||
|
|
||||||
|
- the second argument is also a `string` or a list of `strings`;
|
||||||
|
|
||||||
|
Then the callback is expected to be an ORM callback, only called when the request's
|
||||||
|
`method` *AND* `model` match the ones given in the arguments.
|
||||||
|
|
||||||
|
This works just the same as the above shape, with an added `model` filter:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
onRpc("web_read", "res.partner", ({ args }) => {
|
||||||
|
expect.step(args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
`onRpc`: for *every* ORM method/model
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When the *only* argument is a callback, it is expected to be an ORM callback to
|
||||||
|
be called for *every* ORM call:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
onRpc(({ method }) => {
|
||||||
|
expect.step(method); // Will step every ORM method call on every model
|
||||||
|
});
|
||||||
|
|
||||||
|
Mock models: fields
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Model fields can be declared in 2 ways:
|
||||||
|
|
||||||
|
- as `public class fields <public-class-fields_>`_;
|
||||||
|
|
||||||
|
- under the `_fields` special key. For example:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
test("test view with date fields", async () => {
|
||||||
|
// `_fields` can be assigned over, or extended directly.
|
||||||
|
ResPartner._fields.date = fields.Date({ string: "Registration date" });
|
||||||
|
});
|
||||||
|
|
||||||
|
Field constructors can take a parameters dictionary to dictate their behaviour.
|
||||||
|
It will be required for some of them, like relational fields, which need a `relation`
|
||||||
|
property to work correctly.
|
||||||
|
|
||||||
|
There are limits to what can be done with a mock field compared to an actual Python
|
||||||
|
server field, but expect the most basic properties to be supported:
|
||||||
|
`readonly`, `required`, `string`, etc.
|
||||||
|
|
||||||
|
`compute` and `related` do work for the most basic use-cases, but do not expect
|
||||||
|
them to function reliably as they would on the actual server.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
There are 4 default fields pre-defined for each created model: `id`, `display_name`,
|
||||||
|
`created_at` and `updated_at`. They match their server-side counterpart in their
|
||||||
|
behaviour (e.g. `id` is incremental and `display_name` has a `compute` function
|
||||||
|
similar to its server counterpart), and can be overridden if needed.
|
||||||
|
|
||||||
|
Mock models: records
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Model records are generated based on each object contained in the `_records`
|
||||||
|
special key *when the model is loaded*. They are validated based on the fields available
|
||||||
|
on the current models; if a property does not match a field defined on the model,
|
||||||
|
an error is thrown.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
`_records` *cannot* be altered *after* the model has been loaded, i.e. after
|
||||||
|
the mock server has started. This key is only used to generate initial records.
|
||||||
|
If records should be added *after* model creation, do it either form the available
|
||||||
|
components in the UI, or through direct ORM calls on the mock server instance.
|
||||||
|
|
||||||
|
Mock models: views
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Since actual views need an `"ir.ui.view"` model to be declared, mock models
|
||||||
|
use a simplified *mapping* to provide view arches.
|
||||||
|
|
||||||
|
The `_view` special key is a dictionary, with its *keys* being view types, optionally
|
||||||
|
accompanied by a view ID, and its *values* being the XML arch string representation.
|
||||||
|
|
||||||
|
By default, view IDs are `false`, but can be specified explicitly with a comma-separated
|
||||||
|
key combining the view type and its ID:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
// Will simulate a list view with no ID (false).
|
||||||
|
ResPartner._views.list = /* xml */ `
|
||||||
|
<list>
|
||||||
|
<field name="display_name" />
|
||||||
|
</list>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Will simulate a form view with ID 418.
|
||||||
|
ResPartner._views["form,418"] = /* xml */ `
|
||||||
|
<form>
|
||||||
|
<field name="name" />
|
||||||
|
<field name="date" />
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
.. _mock-server/spawning:
|
||||||
|
|
||||||
|
Spawning a mock server
|
||||||
|
======================
|
||||||
|
|
||||||
|
Just like in most cases, only one server can be active for a given test.
|
||||||
|
|
||||||
|
As mentioned above, creating an `env` will automatically deploy a mock server.
|
||||||
|
|
||||||
|
This means that all of these methods will *also* create a mock server, since
|
||||||
|
they do create an `env`:
|
||||||
|
|
||||||
|
- :ref:`makeMockEnv <web-test-helpers/environment>`;
|
||||||
|
|
||||||
|
- :ref:`mountWithCleanup <web-test-helpers/components>` (calling :ref:`makeMockEnv <web-test-helpers/environment>`);
|
||||||
|
|
||||||
|
- :ref:`mountView <web-test-helpers/views>` (calling :ref:`mountWithCleanup <web-test-helpers/components>`).
|
||||||
|
|
||||||
|
However, some low-level features may require to spawn a mock server *without* an
|
||||||
|
environment. For that purpose, a `makeMockServer` helper can be called separately
|
||||||
|
to initiate a mock server.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
`makeMockServer` should *only* be used by low-level features, such as testing
|
||||||
|
the `rpc` function without the environment. It is not meant to be used as a
|
||||||
|
means to retrieve the current mock server instance. For that purpose, refer to
|
||||||
|
:ref:`MockServer.current <mock-server/interacting>`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is to be noted that subsequent calls to `makeMockServer` after a mock server
|
||||||
|
has been started are simply ignored.
|
||||||
|
|
||||||
|
.. _mock-server/interacting:
|
||||||
|
|
||||||
|
Interacting with the server
|
||||||
|
===========================
|
||||||
|
|
||||||
|
While most of the server interactions are expected to be done directly or indirectly
|
||||||
|
by production code spawned in the test case, it is sometimes meaningful to bypass
|
||||||
|
the UI and call the mock server directly (e.g. to simulate that another user,
|
||||||
|
somewhere else, somehow, has altered the database).
|
||||||
|
|
||||||
|
This can be done by retrieving the `MockServer.current` static property containing
|
||||||
|
the current mock server instance (only after initialization):
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
// Most common ORM methods are provided out of the box by server models,
|
||||||
|
// and are synchronous. Although, be careful that this will NOT trigger a
|
||||||
|
// UI re-render, and will ONLY affect the (fake) database.
|
||||||
|
const ids = MockServer.env["res.partner"].create([
|
||||||
|
{ name: "foo" },
|
||||||
|
{ name: "bar" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
`MockServer.env` is just a shortcut to `MockServer.current.env`.
|
||||||
|
|
||||||
|
.. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
||||||
|
.. _public-class-fields: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields
|
||||||
|
.. _Request: https://developer.mozilla.org/en-US/docs/Web/API/Request
|
||||||
|
.. _Response: https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||||
|
.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
================
|
||||||
|
Web test helpers
|
||||||
|
================
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
After the :doc:`"@odoo/hoot" <./hoot>` module, the second most-solicited module
|
||||||
|
in test files should be `"@web/../tests/web_test_helpers"`.
|
||||||
|
|
||||||
|
This module contains all the helpers that combine the low-level helpers provided
|
||||||
|
by Hoot, with all the most common features that are used in tests in Odoo.
|
||||||
|
|
||||||
|
These helpers are many, and this section of the documentation will only highlight
|
||||||
|
the most common ones, and the way they interact with one another.
|
||||||
|
|
||||||
|
For a full list of available helpers, you may refer to the `web_test_helpers file <{GITHUB_PATH}/addons/web/static/tests/web_test_helpers.js>`_.
|
||||||
|
|
||||||
|
.. _web-test-helpers/environment:
|
||||||
|
|
||||||
|
Mock environment
|
||||||
|
================
|
||||||
|
|
||||||
|
The `makeMockEnv` helper is the lowest helper that can spawn an `env`.
|
||||||
|
|
||||||
|
It will take care of
|
||||||
|
|
||||||
|
- creating the `env` object itself, pre-configured with all the required properties
|
||||||
|
for the proper functioning of web components, such as `getTemplate` or `translateFn`;
|
||||||
|
|
||||||
|
- spawning a `MockServer` (if one did not exist already for that test);
|
||||||
|
|
||||||
|
- starting all registered :doc:`services <../services>`, and awaiting until they are all ready;
|
||||||
|
|
||||||
|
- initiating other features that are not tied to a service, such as the web `router`;
|
||||||
|
|
||||||
|
- guaranteeing the teardown of all the features in its setup at the end of the test.
|
||||||
|
|
||||||
|
This method is great for testing low-level features, such as :doc:`services <../services>` that are
|
||||||
|
not tied to a Component_:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
// Can be further configured, but is already packed with all the necessary stuff
|
||||||
|
const env = await makeMockEnv();
|
||||||
|
|
||||||
|
expect(env.isSmall).toBe(false);
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Like :ref:`makeMockServer <mock-server/spawning>`, only one `env` can be active for a given test.
|
||||||
|
It is not necessary to call `makeMockEnv` manually to retrieve the current environment
|
||||||
|
instance; the `getMockEnv` helper can be called instead.
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
|
||||||
|
- `DateTime input tests <{GITHUB_PATH}/addons/web/static/tests/core/components/datetime/datetime_input.test.js>`_
|
||||||
|
|
||||||
|
- `Name service tests <{GITHUB_PATH}/addons/web/static/tests/core/name_service.test.js>`_
|
||||||
|
|
||||||
|
.. _web-test-helpers/components:
|
||||||
|
|
||||||
|
Mounting components
|
||||||
|
===================
|
||||||
|
|
||||||
|
Instantiating and appending `components <Component_>`_ to the DOM is meant to be easy,
|
||||||
|
through the use of the `mountWithCleanup` helper. It will prepare an `env` internally
|
||||||
|
(if one does not exist yet), which in turn also makes sure that a `MockServer` is
|
||||||
|
running.
|
||||||
|
|
||||||
|
It takes a `Component` class as its first argument, and an *optional* parameters
|
||||||
|
second argument, used to specify `props` or a custom `target`:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
await mountWithCleanup(Checkbox, {
|
||||||
|
props: {
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
This helper will return the active `Component` instance.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
It is generally *ill-advised* to retrieve the `Component` instance to directly
|
||||||
|
interact with it or to perform assertions on its internal variables. The only
|
||||||
|
"accepted" use cases are when the `Component` is displaying hard-to-retrieve information
|
||||||
|
in the DOM, such as graphs in a `canvas <https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API>`_.
|
||||||
|
For most cases, it is highly preferred to query derived information in the DOM.
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
|
||||||
|
- `Checkbox tests <{GITHUB_PATH}/addons/web/static/tests/core/checkbox.test.js>`_
|
||||||
|
|
||||||
|
- `Popover tests <{GITHUB_PATH}/addons/web/static/tests/core/popover/popover.test.js>`_
|
||||||
|
|
||||||
|
- `DateTimePicker tests <{GITHUB_PATH}/addons/web/static/tests/core/components/datetime/datetime_picker.test.js>`_
|
||||||
|
|
||||||
|
.. _web-test-helpers/views:
|
||||||
|
|
||||||
|
Mounting views
|
||||||
|
==============
|
||||||
|
|
||||||
|
Mounting a view is simply a matter of using :ref:`mountWithCleanup <web-test-helpers/components>`
|
||||||
|
with the View_ component and the correct properties.
|
||||||
|
|
||||||
|
For that purpose, web test helpers export a `mountView` helper, taking a parameters
|
||||||
|
object determining the view `type`, `resModel`, and other optional properties such
|
||||||
|
as an XML `arch`:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
// Resolves when the view is fully ready
|
||||||
|
await mountView({
|
||||||
|
type: "list",
|
||||||
|
resModel: "res.partner",
|
||||||
|
arch: /* xml */ `
|
||||||
|
<list>
|
||||||
|
<field name="display_name" />
|
||||||
|
</list>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
Like the previous helpers on top of which `mountView` is built, it will ensure that
|
||||||
|
both an `env` and a `MockServer` are running for the current test.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Like :ref:`mountWithCleanup <web-test-helpers/components>`, it is *NOT*
|
||||||
|
recommended to retrieve the returned View_ component instance. It can however
|
||||||
|
be done, for cases like the `Graph view <{GITHUB_PATH}/addons/web/static/src/views/graph/graph_view.js>`_.
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
|
||||||
|
- `Calendar view tests <{GITHUB_PATH}/addons/web/static/tests/views/calendar/calendar_view.test.js>`_
|
||||||
|
|
||||||
|
- `Graph view tests <{GITHUB_PATH}/addons/web/static/tests/views/graph/graph_view.test.js>`_
|
||||||
|
|
||||||
|
- `Kanban view tests <{GITHUB_PATH}/addons/web/static/tests/views/kanban/kanban_view.test.js>`_
|
||||||
|
|
||||||
|
Interacting with components
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Hoot provides helpers to interact with the DOM (e.g. `click`, `press`, etc.). However,
|
||||||
|
these helpers present 2 issues when interacting with more complex components:
|
||||||
|
|
||||||
|
#. helpers try to interact instantly, while sometimes the element has yet to be
|
||||||
|
appended to the document (in an unknown amount of time);
|
||||||
|
|
||||||
|
#. helpers only wait a single micro-task tick per dispatched event, while most
|
||||||
|
Owl-based UIs take at least a full animation frame to update.
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
// Edit record name
|
||||||
|
await click(".o_field_widget[name=name]");
|
||||||
|
await edit("Gaston Lagaffe");
|
||||||
|
|
||||||
|
// Potential error 1: button may not be in the DOM yet
|
||||||
|
await click(".btn:contains(Save)");
|
||||||
|
|
||||||
|
// Potential error 2: view is not yet updated
|
||||||
|
expect(".o_field_widget[name=name]").toHaveText("Gaston Lagaffe");
|
||||||
|
|
||||||
|
With these constraints in mind, web test helpers provide the `contains` helper:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
// Combines 'click' + 'edit' + 'animationFrame' calls
|
||||||
|
await contains(".o_field_widget[name=name]").edit("Gaston Lagaffe");
|
||||||
|
// Waits for (at least) a full animation frame after the click
|
||||||
|
await contains(".btn:contains(Save)").click();
|
||||||
|
expect(".o_field_widget[name=name]").toHaveText("Gaston Lagaffe");
|
||||||
|
|
||||||
|
This approach, while seemingly drifting a bit further away from the concept of "unit
|
||||||
|
testing", is still a nice and convenient way to test more complex units such as `views <View_>`_,
|
||||||
|
the `WebClient <{GITHUB_PATH}/addons/web/static/src/webclient/webclient.js>`_, or
|
||||||
|
interactions between couples of :doc:`services <../services>` and components.
|
||||||
|
|
||||||
|
It should however not become the default for all interactions, as some of them still
|
||||||
|
need to happen *precisely* within a given time frame, which is a concept completely
|
||||||
|
ignored by `contains`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Most helpers in Hoot are available as methods of a `contains` instance, with
|
||||||
|
(generally) the same shape and API.
|
||||||
|
|
||||||
|
.. _Component: https://github.com/odoo/owl/blob/master/doc/reference/component.md
|
||||||
|
.. _View: {GITHUB_PATH}/addons/web/static/src/views/view.js
|
||||||
Reference in New Issue
Block a user