mirror of
https://github.com/odoo/documentation.git
synced 2025-12-12 07:29:27 +07:00
[IMP] web: HOOT - documentation
Part-of: odoo/documentation#14832 Signed-off-by: Julien Mougenot (jum) <jum@odoo.com>
This commit is contained in:
@@ -21,3 +21,4 @@ Web framework
|
||||
frontend/mobile
|
||||
frontend/qweb
|
||||
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