diff --git a/content/developer/reference/backend/testing.rst b/content/developer/reference/backend/testing.rst
index d3458dc3de..10bf74372c 100644
--- a/content/developer/reference/backend/testing.rst
+++ b/content/developer/reference/backend/testing.rst
@@ -281,252 +281,17 @@ Testing JS code
Testing a complex system is an important safeguard to prevent regressions and to
guarantee that some basic functionality still works. Since Odoo has a non trivial
-codebase in Javascript, it is necessary to test it. In this section, we will
-discuss the practice of testing JS code in isolation: these tests stay in the
-browser, and are not supposed to reach the server.
+codebase in Javascript, it is necessary to test it.
-.. _reference/testing/qunit:
+See the :doc:`Unit testing <../frontend/unit_testing>` to learn about the
+various aspect of the front-end testing framework, or jump directly to one of the
+sub-sections:
-Qunit test suite
-----------------
+- :doc:`Hoot <../frontend/unit_testing/hoot>`
-The Odoo framework uses the QUnit_ library testing framework as a test runner.
-QUnit defines the concepts of *tests* and *modules* (a set of related tests),
-and gives us a web based interface to execute the tests.
+- :doc:`Web test helpers <../frontend/unit_testing/web_helpers>`
-For example, here is what a pyUtils test could look like:
-
-.. code-block:: javascript
-
- QUnit.module('py_utils');
-
- QUnit.test('simple arithmetic', function (assert) {
- assert.expect(2);
-
- var result = pyUtils.py_eval("1 + 2");
- assert.strictEqual(result, 3, "should properly evaluate sum");
- result = pyUtils.py_eval("42 % 5");
- assert.strictEqual(result, 2, "should properly evaluate modulo operator");
- });
-
-The main way to run the test suite is to have a running Odoo server, then
-navigate a web browser to ``/web/tests``. The test suite will then be executed
-by the web browser Javascript engine.
-
-.. image:: testing/tests.png
- :align: center
-
-The web UI has many useful features: it can run only some submodules, or
-filter tests that match a string. It can show every assertions, failed or passed,
-rerun specific tests, ...
-
-.. warning::
-
- While the test suite is running, make sure that:
-
- - your browser window is focused,
- - it is not zoomed in/out. It needs to have exactly 100% zoom level.
-
- If this is not the case, some tests will fail, without a proper explanation.
-
-Testing Infrastructure
-----------------------
-
-Here is a high level overview of the most important parts of the testing
-infrastructure:
-
-- there is an asset bundle named `web.qunit_suite`_. This bundle contains
- the main code (assets common + assets backend), some libraries, the QUnit test
- runner and the test bundles listed below.
-
-- a bundle named `web.tests_assets`_ includes most of the assets and utils required
- by the test suite: custom QUnit asserts, test helpers, lazy loaded assets, etc.
-
-- another asset bundle, `web.qunit_suite_tests`_, contains all the test scripts.
- This is typically where the test files are added to the suite.
-
-- there is a `controller`_ in web, mapped to the route */web/tests*. This controller
- simply renders the *web.qunit_suite* template.
-
-- to execute the tests, one can simply point its browser to the route */web/tests*.
- In that case, the browser will download all assets, and QUnit will take over.
-
-- there is some code in `qunit_config.js`_ which logs in the console some
- information when a test passes or fails.
-
-- we want the runbot to also run these tests, so there is a test (in `test_js.py`_)
- which simply spawns a browser and points it to the *web/tests* url. Note that
- the browser_js method spawns a Chrome headless instance.
-
-
-Modularity and testing
-----------------------
-
-With the way Odoo is designed, any addon can modify the behaviour of other parts
-of the system. For example, the *voip* addon can modify the *FieldPhone* widget
-to use extra features. This is not really good from the perspective of the
-testing system, since this means that a test in the addon web will fail whenever
-the voip addon is installed (note that the runbot runs the tests with all addons
-installed).
-
-At the same time, our testing system is good, because it can detect whenever
-another module breaks some core functionality. There is no complete solution to
-this issue. For now, we solve this on a case by case basis.
-
-Usually, it is not a good idea to modify some other behaviour. For our voip
-example, it is certainly cleaner to add a new *FieldVOIPPhone* widget and
-modify the few views that needs it. This way, the *FieldPhone* widget is not
-impacted, and both can be tested.
-
-Adding a new test case
-----------------------
-
-Let us assume that we are maintaining an addon *my_addon*, and that we
-want to add a test for some javascript code (for example, some utility function
-myFunction, located in *my_addon.utils*). The process to add a new test case is
-the following:
-
-1. create a new file *my_addon/static/tests/utils_tests.js*. This file contains the basic code to
- add a QUnit module *my_addon > utils*.
-
- .. code-block:: javascript
-
- odoo.define('my_addon.utils_tests', function (require) {
- "use strict";
-
- var utils = require('my_addon.utils');
-
- QUnit.module('my_addon', {}, function () {
-
- QUnit.module('utils');
-
- });
- });
-
-
-2. In *my_addon/assets.xml*, add the file to the main test assets:
-
- .. code-block:: xml
-
-
-
-
-
-
-
-
-
-
-3. Restart the server and update *my_addon*, or do it from the interface (to
- make sure the new test file is loaded)
-
-4. Add a test case after the definition of the *utils* sub test suite:
-
- .. code-block:: javascript
-
- QUnit.test("some test case that we want to test", function (assert) {
- assert.expect(1);
-
- var result = utils.myFunction(someArgument);
- assert.strictEqual(result, expectedResult);
- });
-
-5. Visit */web/tests/* to make sure the test is executed
-
-Helper functions and specialized assertions
--------------------------------------------
-
-Without help, it is quite difficult to test some parts of Odoo. In particular,
-views are tricky, because they communicate with the server and may perform many
-rpcs, which needs to be mocked. This is why we developed some specialized
-helper functions, located in `test_utils.js`_.
-
-- Mock test functions: these functions help setting up a test environment. The
- most important use case is mocking the answers given by the Odoo server. These
- functions use a `mock server`_. This is a javascript class that simulates
- answers to the most common model methods: read, search_read, nameget, ...
-
-- DOM helpers: useful to simulate events/actions on some specific target. For
- example, testUtils.dom.click performs a click on a target. Note that it is
- safer than doing it manually, because it also checks that the target exists,
- and is visible.
-
-- create helpers: they are probably the most important functions exported by
- `test_utils.js`_. These helpers are useful to create a widget, with a mock
- environment, and a lot of small detail to simulate as much as possible the
- real conditions. The most important is certainly `createView`_.
-
-- `qunit assertions`_: QUnit can be extended with specialized assertions. For
- Odoo, we frequently test some DOM properties. This is why we made some
- assertions to help with that. For example, the *containsOnce* assertion takes
- a widget/jQuery/HtmlElement and a selector, then checks if the target contains
- exactly one match for the css selector.
-
-For example, with these helpers, here is what a simple form test could look like:
-
-.. code-block:: javascript
-
- QUnit.test('simple group rendering', function (assert) {
- assert.expect(1);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'partner',
- data: this.data,
- arch: '
',
- res_id: 1,
- });
-
- assert.containsOnce(form, 'table.o_inner_group');
-
- form.destroy();
- });
-
-Notice the use of the testUtils.createView helper and of the containsOnce
-assertion. Also, the form controller was properly destroyed at the end of
-the test.
-
-Best Practices
---------------
-
-In no particular order:
-
-- all test files should be added in *some_addon/static/tests/*
-- for bug fixes, make sure that the test fails without the bug fix, and passes
- with it. This ensures that it actually works.
-- try to have the minimal amount of code necessary for the test to work.
-- usually, two small tests are better than one large test. A smaller test is
- easier to understand and to fix.
-- always cleanup after a test. For example, if your test instantiates a widget,
- it should destroy it at the end.
-- no need to have full and complete code coverage. But adding a few tests helps
- a lot: it makes sure that your code is not completely broken, and whenever a
- bug is fixed, it is really much easier to add a test to an existing test suite.
-- if you want to check some negative assertion (for example, that a HtmlElement
- does not have a specific css class), then try to add the positive assertion in
- the same test (for example, by doing an action that changes the state). This
- will help avoid the test to become dead in the future (for example, if the css
- class is changed).
-
-Tips
-----
-
-- running only one test: you can (temporarily!) change the *QUnit.test(...)*
- definition into *QUnit.only(...)*. This is useful to make sure that QUnit
- only runs this specific test.
-- debug flag: most create utility functions have a debug mode (activated by the
- debug: true parameter). In that case, the target widget will be put in the DOM
- instead of the hidden qunit specific fixture, and more information will be
- logged. For example, all mocked network communications will be available in the
- console.
-- when working on a failing test, it is common to add the debug flag, then
- comment the end of the test (in particular, the destroy call). With this, it
- is possible to see the state of the widget directly, and even better, to
- manipulate the widget by clicking/interacting with it.
+- :doc:`Mock server <../frontend/unit_testing/mock_server>`
.. _reference/testing/integration-testing:
@@ -982,8 +747,6 @@ you can use the :meth:`~odoo.tests.BaseCase.assertQueryCount` method, integrated
.. _qunit: https://qunitjs.com/
.. _qunit_config.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/qunit_config.js#L49
.. _web.tests_assets: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L594
-.. _web.qunit_suite: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L660
-.. _web.qunit_suite_tests: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/views/webclient_templates.xml#L680
.. _controller: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/controllers/main.py#L637
.. _test_js.py: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/tests/test_js.py#L13
.. _test_utils.js: https://github.com/odoo/odoo/blob/51ee0c3cb59810449a60dae0b086b49b1ed6f946/addons/web/static/tests/helpers/test_utils.js
diff --git a/content/developer/reference/frontend.rst b/content/developer/reference/frontend.rst
index e2b7fae340..698fd26dc8 100644
--- a/content/developer/reference/frontend.rst
+++ b/content/developer/reference/frontend.rst
@@ -21,3 +21,4 @@ Web framework
frontend/mobile
frontend/qweb
frontend/odoo_editor
+ frontend/unit_testing
diff --git a/content/developer/reference/frontend/assets.rst b/content/developer/reference/frontend/assets.rst
index c4d912842d..7f28b412c4 100644
--- a/content/developer/reference/frontend/assets.rst
+++ b/content/developer/reference/frontend/assets.rst
@@ -75,8 +75,8 @@ like this:
'web/static/src/js/webclient.js',
'web/static/src/xml/webclient.xml',
],
- 'web.qunit_suite_tests': [
- 'web/static/src/js/webclient_tests.js',
+ 'web.assets_unit_tests': [
+ 'web/static/src/js/webclient.test.js',
],
},
@@ -94,10 +94,7 @@ know:
- `web.assets_frontend`: this bundle is about all that is specific to the public
website: ecommerce, portal, forum, blog, ...
-- `web.qunit_suite_tests`: all javascript qunit testing code (tests, helpers, mocks)
-
-- `web.qunit_mobile_suite_tests`: mobile specific qunit testing code
-
+- `web.assets_unit_tests`: all javascript unit testing code (tests, helpers, mocks)
Operations
----------
diff --git a/content/developer/reference/frontend/unit_testing.rst b/content/developer/reference/frontend/unit_testing.rst
new file mode 100644
index 0000000000..957cb114d1
--- /dev/null
+++ b/content/developer/reference/frontend/unit_testing.rst
@@ -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
diff --git a/content/developer/reference/frontend/unit_testing/hoot.rst b/content/developer/reference/frontend/unit_testing/hoot.rst
new file mode 100644
index 0000000000..8fb4a816fe
--- /dev/null
+++ b/content/developer/reference/frontend/unit_testing/hoot.rst
@@ -0,0 +1,1662 @@
+====
+HOOT
+====
+
+Overview
+========
+
+:abbr:`HOOT (Hierarchically Organized Odoo Tests)` is a testing framework written with Owl whose
+key features are:
+
+- to register and run tests and test suites;
+- to display an intuitive interface to view and filter test results;
+- to provide ways to interact with the DOM to simulate user actions;
+- to provide low-level helpers allowing to mock various global objects.
+
+As such, it has been integrated as a :file:`lib/` in the Odoo codebase and exports 2 main modules:
+
+- :file:`@odoo/hoot-dom`: (can be used in tours) helpers to:
+
+ - **interact** with the DOM, such as :js:meth:`click` and :js:meth:`press`;
+ - **query** elements from the DOM, such as :js:meth:`queryAll` and :js:meth:`waitFor`;
+
+- :file:`@odoo/hoot`: (only to be used in unit tests) all the test framework features:
+
+ - `test`, `describe` and `expect`
+ - test hooks like `after` and `afterEach`
+ - fixture handling with `getFixture`
+ - date and time handling like `mockDate` or `advanceTime`
+ - mocking network responses through :js:meth:`mockFetch` or :js:meth:`mockWebSocket`
+ - every helper exported by :file:`@odoo/hoot-dom`
+
+.. note::
+ This section of the documentation is not meant to list *all* helpers available
+ in Hoot (the full list can be found in the `@odoo/hoot <{GITHUB_PATH}/addons/web/static/lib/hoot/hoot.js>`_
+ module itself). The goal here is to showcase the most used helpers and to justify
+ some of the decisions that have led to the current shape of the testing framework.
+
+Running tests
+=============
+
+In Odoo, frontend unit tests can be run by going to the `/web/tests` URL. Most of
+the setup for calling the test runner is already in place:
+
+- the `web.assets_unit_tests` bundle is already defined, and picks up all tests
+ defined in most addons;
+
+- a :file:`start.hoot.js` file takes care of calling the test runner with its exported
+ `start` entry point function.
+
+When going to the test page, tests will be run sequentially and the results will
+be displayed in the console and in the GUI (if not running in `headless` mode).
+
+Runner options
+--------------
+
+The runner can be configured either:
+
+- through the interface (with the configuration dropdown and the search bar);
+- or through the URL query parameters (e.g. `?headless` to run in headless mode).
+
+Here is the list of available options for the runner:
+
+- `bail`
+ Amount of failed tests after which the test runner will be stopped. A falsy value
+ (including 0) means that the runner should never be aborted. (default: `0`)
+
+- `debugTest`
+ Same as the `FILTER_SCHEMA.test` filter, while also putting the test runner in
+ "debug" mode. See `TestRunner.debug` for more info. (default: `false`)
+
+- `fps`
+ Sets the value of frames per seconds (this will be transformed to milliseconds and used in
+ `advanceFrame`)
+
+- `filter`
+ Search string that will filter matching tests/suites, based on their full name (including
+ their parent suite(s)) and their tags. (default: `""`)
+
+- `frameRate`
+ *Estimated* amount of frames rendered per second, used when mocking animation frames. (default:
+ `60` fps)
+
+- `fun`
+ Lightens the mood. (default: `false`)
+
+- `headless`
+ Whether to render the test runner user interface. (default: `false`)
+
+- `id`
+ IDs of the suites OR tests to run exclusively. The ID of a job is generated
+ deterministically based on its full name.
+
+- `loglevel`
+ Log level used by the test runner. The higher the level, the more logs will be displayed:
+
+ - `0`: only runner logs are displayed (default)
+ - `1`: all suite results are also logged
+ - `2`: all test results are also logged
+ - `3`: debug information for each test is also logged
+
+- `manual`
+ Whether the test runner must be manually started after page load (defaults to starting
+ automatically). (default: `false`)
+
+- `notrycatch`
+ Removes the safety of `try .. catch` statements around each test's run function to let errors
+ bubble to the browser. (default: `false`)
+
+- `order`
+ Determines the order of test execution:
+
+ - `"fifo"`: tests will be run sequentially as declared in the file system;
+ - `"lifo"`: tests will be run sequentially in the reverse order;
+ - `"random"`: shuffles tests and suites within their parent suite.
+
+- `preset`
+ Environment in which the test runner is running. This parameter is used to
+ determine the default value of other features, namely:
+
+ - the user agent;
+ - touch support;
+ - expected size of the viewport.
+
+- `showdetail`
+ Determines how the failed tests must be unfolded in the UI. (default: `"first-fail"`)
+
+- `tag`
+ Tag names of tests and suites to run exclusively (case insensitive). (default: empty)
+
+- `timeout`
+ Duration (in milliseconds) at the end of which a test will automatically fail.
+ (default: `5` seconds)
+
+.. note::
+ When selecting tests and suites to run, an implicit `OR` is applied between
+ the *including* filters. This means that adding more inclusive filters will
+ result in more tests being run. This applies to the `filter`, `id` and `tag`
+ filters (*excluding* filters however will remove matching tests from the list
+ of tests to run).
+
+
+Writing tests
+=============
+
+Test
+----
+
+Writing a test can be very straightforward, as it is just a matter of calling the `test` function
+with a name and a function that will contain the test logic.
+
+Here is a simple example:
+
+.. code-block:: javascript
+
+ import { expect, test } from "@odoo/hoot";
+
+ test("My first test", () => {
+ expect(2 + 2).toBe(4);
+ });
+
+
+Describe
+--------
+
+Most of the time, tests are not that simple. They often require some setup and teardown,
+and sometimes they need to be grouped together in a suite. This is where the `describe`
+function comes into play.
+
+Here is how you would declare a suite and a test within it:
+
+.. code-block:: javascript
+
+ import { describe, expect, test } from "@odoo/hoot";
+
+ describe("My first suite", () => {
+ test("My first test", () => {
+ expect(2 + 2).toBe(4);
+ });
+ });
+
+.. important::
+ In Odoo, all test files are run in an isolated environment and are wrapped within a global
+ `describe` block (with the name of the suite being the *path* of the test file).
+
+ With that in
+ mind you should not need to declare a suite in your test files, although you can still declare
+ sub-suites in the same file if you still want to split the file's suite, for organization
+ or tagging purposes.
+
+
+Expect
+======
+
+The `expect` function is the main assertion function of the framework. It is used
+to assert that a value or an object is what it is expected to be or in the state
+it is supposed to be. To do so, it provides a few modifiers and a wide range of
+matchers.
+
+
+Modifiers
+---------
+
+An `expect` modifier is a getter that returns another set of *altered* matchers that will behave in
+a specific way.
+
+- `not`
+ Inverts the result of the following matcher: it will succeed if the matcher fails.
+
+ .. code-block:: javascript
+
+ expect(true).not.toBe(false);
+
+- `resolves`
+ Waits for the value (`Promise`) to be *"resolved"* before running the following
+ matcher with the resolved value.
+
+ .. code-block:: javascript
+
+ await expect(Promise.resolve(42)).resolves.toBe(42);
+
+- `rejects`
+ Waits for the value (`Promise`) to be *"rejected"* before running the following
+ matcher with the rejected reason.
+
+ .. code-block:: javascript
+
+ await expect(Promise.reject("error")).rejects.toBe("error");
+
+.. note::
+ The `resolves` and `rejects` modifiers are only available when the value is
+ a promise, and will return a promise that will resolve once the assertion is
+ done.
+
+
+Regular matchers
+----------------
+
+The matchers dictate what to do on the value being tested. Some will take that value
+as-is, while others will *transform* that value before performing the assertion
+on it (i.e. DOM matchers).
+
+Note that the last argument parameter of all matchers is an optional dictionary with additional
+options, in which a custom assertion `message` can be given for added context/specificity.
+
+The first list of matchers are primitive or object based and are the most common ones:
+
+.. js:method:: toBe(expected[, options])
+
+ Expects the received value to be *strictly equal* to the `expected` value.
+
+ - Parameters
+
+ * `expected`: `any`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("foo").toBe("foo");
+ expect({ foo: 1 }).not.toBe({ foo: 1 });
+
+.. js:method:: toBeCloseTo(expected[, options])
+
+ Expects the received value to be *close to* the `expected` value up to a given
+ amount of digits (default is 2).
+
+ - Parameters
+
+ * `expected`: `any`
+ * `options`: `{ message?: string, digits?: number }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(0.2 + 0.1).toBeCloseTo(0.3);
+ expect(3.51).toBeCloseTo(3.5, { digits: 1 });
+
+.. js:method:: toBeEmpty([options])
+
+ Expects the received value to be empty:
+
+ - `iterable`: no items
+ - `object`: no keys
+ - `node`: no content (i.e. no value or text)
+ - anything else: falsy value (`false`, `0`, `""`, `null`, `undefined`)
+
+ - Parameters
+
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect({}).toBeEmpty();
+ expect(["a", "b"]).not.toBeEmpty();
+ expect(queryOne("input")).toBeEmpty();
+
+.. js:method:: toBeGreaterThan(min[, options])
+
+ Expects the received value to be *strictly greater* than `min`.
+
+ - Parameters
+
+ * `min`: `number`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(5).toBeGreaterThan(-1);
+ expect(4 + 2).toBeGreaterThan(5);
+
+.. js:method:: toBeInstanceOf(cls[, options])
+
+ Expects the received value to be an instance of the given `cls`.
+
+ - Parameters
+
+ * `cls`: `Function`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect({ foo: 1 }).not.toBeInstanceOf(Object);
+ expect(document.createElement("div")).toBeInstanceOf(HTMLElement);
+
+.. js:method:: toBeLessThan(max[, options])
+
+ Expects the received value to be *strictly less* than `max`.
+
+ - Parameters
+
+ * `max`: `number`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(5).toBeLessThan(10);
+ expect(8 - 6).toBeLessThan(3);
+
+.. js:method:: toBeOfType(type[, options])
+
+ Expects the received value to be of the given `type`.
+
+ - Parameters
+
+ * `type`: `string`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("foo").toBeOfType("string");
+ expect({ foo: 1 }).toBeOfType("object");
+
+.. js:method:: toBeWithin(min, max[, options])
+
+ Expects the received value to be *between* `min` and `max` (both inclusive).
+
+ - Parameters
+
+ * `min`: `number`
+ * `max`: `number`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(3).toBeWithin(3, 9);
+ expect(-8.5).toBeWithin(-20, 0);
+ expect(100).toBeWithin(50, 100);
+
+.. js:method:: toEqual(expected[, options])
+
+ Expects the received value to be *deeply equal* to the `expected` value.
+
+ - Parameters
+
+ * `expected`: `any`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(["foo"]).toEqual(["foo"]);
+ expect({ foo: 1 }).toEqual({ foo: 1 });
+
+.. js:method:: toHaveLength(length[, options])
+
+ Expects the received value to have a length of the given `length`.
+ Received value can be any `Iterable` or `Object`.
+
+ - Parameters
+
+ * `length`: `number`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("foo").toHaveLength(3);
+ expect([1, 2, 3]).toHaveLength(3);
+ expect({ foo: 1, bar: 2 }).toHaveLength(2);
+ expect(new Set([1, 2])).toHaveLength(2);
+
+.. js:method:: toInclude(item[, options])
+
+ Expects the received value to include an `item` of a given shape.
+
+ Received value can be an iterable or an object (in case it is an object,
+ the `item` should be a key or a tuple representing an entry in that object).
+
+ Note that it is NOT a strict comparison: the item will be matched for deep
+ equality against each item of the iterable.
+
+ - Parameters
+
+ * `item`: `any`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect([1, 2, 3]).toInclude(2);
+ expect({ foo: 1, bar: 2 }).toInclude("foo");
+ expect({ foo: 1, bar: 2 }).toInclude(["foo", 1]);
+ expect(new Set([{ foo: 1 }, { bar: 2 }])).toInclude({ bar: 2 });
+
+.. js:method:: toMatch(matcher[, options])
+
+ Expects the received value to match the given `matcher`.
+
+ - Parameters
+
+ * `matcher`: `string | number | RegExp`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(new Error("foo")).toMatch("foo");
+ expect("a foo value").toMatch(/fo.*ue/);
+
+.. js:method:: toThrow(matcher[, options])
+
+ Expects the received `Function` to throw an error after being called.
+
+ - Parameters
+
+ * `matcher`: `string | number | RegExp`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(() => { throw new Error("Woops!") }).toThrow(/woops/i);
+ await expect(Promise.reject("foo")).rejects.toThrow("foo");
+
+
+DOM matchers
+------------
+
+This next list of matchers are node-based and are used to assert the state of a
+node or a list of nodes. They generally take a :ref:`custom selector `
+as the argument of the `expect` function (although a `Node` or an iterable of `Node`
+is also accepted).
+
+.. js:method:: toBeChecked([options])
+
+ Expects the received `Target` to be `"checked"`, or to be `"indeterminate"`
+ if the homonymous option is set to `true`.
+
+ - Parameters
+
+ * `options`: `{ message?: string, indeterminate?: boolean }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("input[type=checkbox]").toBeChecked();
+
+.. js:method:: toBeDisplayed([options])
+
+ Expects the received `Target` to be *"displayed"*, meaning that:
+
+ - it has a bounding box;
+ - it is contained in the root document.
+
+ - Parameters
+
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(document.body).toBeDisplayed();
+ expect(document.createElement("div")).not.toBeDisplayed();
+
+.. js:method:: toBeEnabled([options])
+
+ Expects the received `Target` to be *"enabled"*, meaning that it
+ matches the `:enabled` pseudo-selector.
+
+ - Parameters
+
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("button").toBeEnabled();
+ expect("input[type=radio]").not.toBeEnabled();
+
+.. js:method:: toBeFocused([options])
+
+ Expects the received `Target` to be *"focused"* in its owner document.
+
+ - Parameters
+
+ * `options`: `{ message?: string }`
+
+.. js:method:: toBeVisible([options])
+
+ Expects the received `Target` to be *"visible"*, meaning that:
+
+ - it has a bounding box;
+ - it is contained in the root document;
+ - it is not hidden by CSS properties.
+
+ - Parameters
+
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(document.body).toBeVisible();
+ expect("[style='opacity: 0']").not.toBeVisible();
+
+.. js:method:: toHaveAttribute(attribute, value[, options])
+
+ Expects the received `Target` to have the given attribute set, and for that
+ attribute value to match the given `value` if any.
+
+ - Parameters
+
+ * `attribute`: `string`
+ * `value`: `string | number | RegExp`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("a").toHaveAttribute("href");
+ expect("script").toHaveAttribute("src", "./index.js");
+
+.. js:method:: toHaveClass(className[, options])
+
+ Expects the received `Target` to have the given class name(s).
+
+ - Parameters
+
+ * `className`: `string | string[]`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("button").toHaveClass("btn btn-primary");
+ expect("body").toHaveClass(["o_webclient", "o_dark"]);
+
+.. js:method:: toHaveCount(amount[, options])
+
+ Expects the received `Target` to contain exactly `amount` element(s).
+ Note that the `amount` parameter can be omitted, in which case the function
+ will expect *at least* one element.
+
+ - Parameters
+
+ * `amount`: `number`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(".o_webclient").toHaveCount(1);
+ expect(".o_form_view .o_field_widget").toHaveCount();
+ expect("ul > li").toHaveCount(4);
+
+.. js:method:: toHaveInnerHTML(expected[, options])
+
+ Expects the `innerHTML` of the received `Target` to match the `expected`
+ value (upon formatting).
+
+ - Parameters
+
+ * `expected`: `string | RegExp`
+ * `options`: `{ message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(".my_element").toHaveInnerHTML(`
+ Some text
+ `);
+
+.. js:method:: toHaveOuterHTML(expected[, options])
+
+ Expects the `outerHTML` of the received `Target` to match the `expected`
+ value (upon formatting).
+
+ - Parameters
+
+ * `expected`: `string | RegExp`
+ * `options`: `{ message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect(".my_element").toHaveOuterHTML(`
+
+ Some text
+
+ `);
+
+.. js:method:: toHaveProperty(property, value[, options])
+
+ Expects the received `Target` to have its given property value match
+ the given `value`. If no value is given: the matcher will instead check that
+ the given property exists on the target.
+
+ - Parameters
+
+ * `property`: `string`
+ * `value`: `any`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("button").toHaveProperty("tabIndex", 0);
+ expect("input").toHaveProperty("ontouchstart");
+ expect("script").toHaveProperty("src", "./index.js");
+
+.. js:method:: toHaveRect(rect[, options])
+
+ Expects the `DOMRect` of the received `Target` to match the given `rect` object.
+ The `rect` object can either be:
+
+ - a `DOMRect` object;
+ - a CSS selector string (to get the rect of the *only* matching element);
+ - a node.
+
+ If the resulting `rect` value is a node, then both nodes' rects will be compared.
+
+ - Parameters
+
+ * `rect`: `Partial | Target`
+ * `options`: `{ message?: string, trimPadding?: boolean }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("button").toHaveRect({ x: 20, width: 100, height: 50 });
+ expect("button").toHaveRect(".container");
+
+.. js:method:: toHaveStyle(style[, options])
+
+ Expects the received `Target` to match the given style properties.
+
+ - Parameters
+
+ * `style`: `string | Record`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("button").toHaveStyle({ color: "red" });
+ expect("p").toHaveStyle("text-align: center");
+
+.. js:method:: toHaveText(text[, options])
+
+ Expects the `text` content of the received `Target` to either:
+
+ - be strictly equal to a given string;
+ - match a given regular expression.
+
+ Note: `innerHTML` is used to retrieve the text content to take CSS visibility
+ into account. This also means that text values from child elements will be
+ joined using a line-break as separator.
+
+ - Parameters
+
+ * `text`: `string | RegExp`
+ * `options`: `{ message?: string, raw?: boolean }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("p").toHaveText("lorem ipsum dolor sit amet");
+ expect("header h1").toHaveText(/odoo/i);
+
+.. js:method:: toHaveValue(value[, options])
+
+ Expects the value of the received `Target` to either:
+
+ - be strictly equal to a given string or number;
+ - match a given regular expression;
+ - contain file objects matching the given `files` list.
+
+ - Parameters
+
+ * `value`: `any`
+ * `options`: `{ message?: string }`
+
+ - Examples
+
+ .. code-block:: javascript
+
+ expect("input[type=email]").toHaveValue("john@doe.com");
+ expect("input[type=file]").toHaveValue(new File(["foo"], "foo.txt"));
+ expect("select[multiple]").toHaveValue(["foo", "bar"]);
+
+Static methods
+--------------
+
+The `expect` helper function also contains static methods that can be used to run
+through a detached testing flow that isn't bound to one specific value at a certain
+moment.
+
+These methods are mainly used to register steps or errors in the scope of the current
+test, and to evaluate them later on.
+
+.. js:function:: expect.assertions(expected)
+
+ :param number expected:
+
+ Expects the current test to have the `expected` amount of assertions. This
+ number cannot be less than 1.
+
+ .. note::
+ It is generally preferred to use :js:meth:`expect.step` and :js:meth:`expect.verifySteps`
+ instead as it is more reliable and allows to test more extensively.
+
+.. js:function:: expect.errors(expected)
+
+ :param number expected:
+
+ Expects the current test to have the `expected` amount of errors.
+
+ This also means that from the moment this function is called, the test will
+ accept that amount of errors before being considered as failed.
+
+.. js:function:: expect.step(value)
+
+ :param unknown value:
+
+ Registers a step for the current test, that can be consumed by :js:meth:`expect.verifySteps`.
+ Unconsumed steps will fail the test.
+
+.. js:function:: expect.verifyErrors(errors[, options])
+
+ :param unknown[] errors:
+ :param { message?\: string } options:
+ :returns: `boolean`
+
+ Expects the received matchers to match the errors thrown since the start of
+ the test or the last call to :js:meth:`expect.verifyErrors`. Calling this matcher
+ will reset the list of current errors.
+
+ .. code-block:: javascript
+
+ expect.verifyErrors([/RPCError/, /Invalid domain AST/]);
+
+.. js:function:: expect.verifySteps(steps[, options])
+
+ :param unknown[] steps:
+ :param { ignoreOrder?\: boolean, message?\: string, partial?\: boolean } options:
+ :returns: `boolean`
+
+ Expects the received steps to be equal to the steps emitted since the start
+ of the test or the last call to :js:meth:`expect.verifySteps`. Calling this
+ matcher will reset the list of current steps.
+
+ .. code-block:: javascript
+
+ expect.step("web_read_group");
+ expect.step([1, 2]);
+ expect.verifySteps(["web_read_group", [1, 2]]);
+
+.. js:function:: expect.waitForErrors(errors[, options])
+
+ :param unknown[] errors:
+ :param { message?\: string } options:
+ :returns: `Promise`
+
+ Same as :js:meth:`expect.verifyErrors`, but will not immediatly fail if errors
+ are not caught yet, and will instead wait for a certain timeout (default: 2000ms)
+ to allow errors to be caught later.
+
+ Checks are performed initially, at the end of the timeout, and each time an
+ error is detected.
+
+ .. code-block:: javascript
+
+ fetch("invalid/url");
+ await expect.waitForErrors([/RPCError/]);
+
+.. js:function:: expect.waitForSteps(steps[, options])
+
+ :param unknown[] steps:
+ :param { ignoreOrder?\: boolean, message?\: string, partial?\: boolean } options:
+ :returns: `Promise`
+
+ Same as :js:meth:`expect.verifySteps`, but will not immediatly fail if steps
+ have not been registered yet, and will instead wait for a certain timeout (default:
+ 2000ms) to allow steps to be registered later.
+
+ Checks are performed initially, at the end of the timeout, and each time
+ a step is registered.
+
+ .. code-block:: javascript
+
+ // ... step on each 'web_read_group' call
+ fetch(".../call_kw/web_read_group");
+ await expect.waitForSteps(["web_read_group"]);
+
+DOM: queries
+============
+
+.. _hoot/custom-dom-selectors:
+
+Custom DOM selectors
+--------------------
+
+Here's a brief section on DOM selectors in Hoot, as they support additional pseudo-classes
+that can be used to target elements based on non-standard features, such as their
+text content or their global position in the document.
+
+- `:contains(text)`
+ matches nodes whose text content matches the given `text`
+
+ - given *text* supports regular expression syntax (e.g. `:contains(/^foo.+/)`) and is
+ case-insensitive (unless using the `i` flag at the end of the regex)
+
+- `:displayed`
+ matches nodes that are *"displayed"* (see `isDisplayed`)
+
+- `:empty`
+ matches nodes that have an empty content (value or text content)
+
+- `:eq(n)`
+ returns the *nth* node based on its global position (0-based index);
+
+- `:first`
+ returns the first node matching the selector (in the whole document)
+
+- `:focusable`
+ matches nodes that can be *"focused"* (see `isFocusable`)
+
+- `:hidden`
+ matches nodes that are *not* *"visible"* (see `isVisible`)
+
+- `:iframe`
+ matches nodes that are `