1679 lines
49 KiB
ReStructuredText
1679 lines
49 KiB
ReStructuredText
====
|
|
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 3 main modules:
|
|
|
|
- :file:`@odoo/hoot`: main building blocks of the framwork, such as:
|
|
|
|
- `test`, `describe` and `expect`
|
|
- test hooks like `after` and `afterEach`
|
|
- fixture handling with `getFixture`
|
|
|
|
- :file:`@odoo/hoot-dom`: helpers to:
|
|
|
|
- **interact** with the DOM, such as :ref:`click <hoot/click>` and :ref:`press <hoot/press>`;
|
|
- **query** elements from the DOM, such as :ref:`queryAll <hoot/query-all>`
|
|
and :ref:`waitFor <hoot/wait-for>`;
|
|
|
|
- :file:`@odoo/hoot-mock`: helpers to mock default behaviours and objects, such as:
|
|
|
|
- date and time handling like `mockDate` or `advanceTime`
|
|
- mocking network responses through :ref:`mockFetch <hoot/mock-fetch>` or :ref:`mockWebSocket <hoot/mock-websocket>`
|
|
|
|
|
|
Running tests
|
|
=============
|
|
|
|
To setup the test runner is quite straightforward: you just need to have all the lib files loaded
|
|
in the assets bundle along with Owl, and then call the main `start` method entrypoint once all
|
|
tests and suites have been registered. The tests will then 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`)
|
|
|
|
- `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 tests 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 the tests 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"`)
|
|
|
|
- `suite`
|
|
**IDs** of the suites to run exclusively. The ID of a suite is an 8-character hash generated
|
|
deterministically based on its full name. (default: emtpy)
|
|
|
|
- `tag`
|
|
Tag **names** of tests and suites to run exclusively (case insensitive). (default: empty)
|
|
|
|
- `test`
|
|
**IDs** of the tests to run exclusively. The ID of a test is an 8-character hash generated
|
|
deterministically based on its full name. (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`, `suite`, `tag` and `test` 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 organisation
|
|
or tagging purpose.
|
|
|
|
|
|
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 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 *tranform* 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:
|
|
|
|
#. `toBe`
|
|
|
|
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 });
|
|
|
|
#. `toBeCloseTo`
|
|
|
|
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 });
|
|
|
|
#. `toBeEmpty`
|
|
|
|
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();
|
|
|
|
#. `toBeGreaterThan`
|
|
|
|
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);
|
|
|
|
#. `toBeInstanceOf`
|
|
|
|
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);
|
|
|
|
#. `toBeLessThan`
|
|
|
|
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);
|
|
|
|
#. `toBeOfType`
|
|
|
|
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");
|
|
|
|
#. `toBeWithin`
|
|
|
|
Expects the received value to be **strictly 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);
|
|
|
|
#. `toEqual`
|
|
|
|
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 });
|
|
|
|
#. `toHaveLength`
|
|
|
|
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);
|
|
|
|
#. `toInclude`
|
|
|
|
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 });
|
|
|
|
#. `toMatch`
|
|
|
|
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/);
|
|
|
|
#. `toThrow`
|
|
|
|
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 <hoot/custom-dom-selectors>`
|
|
as the argument of the `expect` function (although a `Node` or an iterable of `Node`
|
|
is also accepted).
|
|
|
|
#. `toBeChecked`
|
|
|
|
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();
|
|
|
|
#. `toBeDisplayed`
|
|
|
|
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();
|
|
|
|
#. `toBeEnabled`
|
|
|
|
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();
|
|
|
|
#. `toBeFocused`
|
|
|
|
Expects the received `Target` to be **focused** in its owner document.
|
|
|
|
- Parameters
|
|
|
|
* `options`: `{ message?: string }`
|
|
|
|
#. `toBeVisible`
|
|
|
|
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();
|
|
|
|
#. `toHaveAttribute`
|
|
|
|
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");
|
|
|
|
#. `toHaveClass`
|
|
|
|
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"]);
|
|
|
|
#. `toHaveCount`
|
|
|
|
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);
|
|
|
|
#. `toHaveInnerHTML`
|
|
|
|
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 <strong>text</strong>
|
|
`);
|
|
|
|
#. `toHaveOuterHTML`
|
|
|
|
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(`
|
|
<div class="my_element">
|
|
Some <strong>text</strong>
|
|
</div>
|
|
`);
|
|
|
|
#. `toHaveProperty`
|
|
|
|
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");
|
|
|
|
#. `toHaveRect`
|
|
|
|
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<DOMRect> | Target`
|
|
* `options`: `{ message?: string, trimPadding?: boolean }`
|
|
|
|
- Examples
|
|
|
|
.. code-block:: javascript
|
|
|
|
expect("button").toHaveRect({ x: 20, width: 100, height: 50 });
|
|
expect("button").toHaveRect(".container");
|
|
|
|
#. `toHaveStyle`
|
|
|
|
Expects the received `Target` to match the given **style** properties.
|
|
|
|
- Parameters
|
|
|
|
* `style`: `string | Record<string, string | RegExp>`
|
|
* `options`: `{ message?: string }`
|
|
|
|
- Examples
|
|
|
|
.. code-block:: javascript
|
|
|
|
expect("button").toHaveStyle({ color: "red" });
|
|
expect("p").toHaveStyle("text-align: center");
|
|
|
|
#. `toHaveText`
|
|
|
|
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);
|
|
|
|
#. `toHaveValue`
|
|
|
|
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"]);
|
|
|
|
|
|
DOM
|
|
===
|
|
|
|
.. _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 **inner text**)
|
|
|
|
- `: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 `<iframe>` elements, and returns their `body` if it is ready
|
|
|
|
- `:last`
|
|
returns the **last** node matching the selector (in the whole document)
|
|
|
|
- `:selected`
|
|
matches nodes that are **selected** (e.g. `<option>` elements)
|
|
|
|
- `:shadow`
|
|
matches nodes that have shadow roots, and returns their shadow root
|
|
|
|
- `:scrollable`
|
|
matches nodes that are scrollable (see `isScrollable`)
|
|
|
|
- `:value(text)`
|
|
matches nodes whose **value** matches the given **text**
|
|
|
|
- given *text* supports regular expression syntax (e.g. `:value(/^foo.+/)`) and is
|
|
case-insensitive (unless using the **i** flag at the end of the regex)
|
|
|
|
- `:visible`
|
|
matches nodes that are **visible** (see `isVisible`)
|
|
|
|
Query & node properties helpers
|
|
-------------------------------
|
|
|
|
.. js:function:: getActiveElement([node])
|
|
|
|
Returns the currently focused element in the document.
|
|
|
|
:returns: the currently focused element
|
|
|
|
.. js:function:: getFocusableElements([options])
|
|
|
|
Returns the list of focusable elements in the given parent, sorted by their `tabIndex`
|
|
property.
|
|
|
|
:returns: the list of focusable elements
|
|
|
|
.. js:function:: getNextFocusableElement([options])
|
|
|
|
Returns the next focusable element after the current active element if it is contained in the
|
|
given parent.
|
|
|
|
:returns: the next focusable element
|
|
|
|
.. js:function:: getPreviousFocusableElement([options])
|
|
|
|
Returns the previous focusable element before the current active element if it is contained in
|
|
the given parent.
|
|
|
|
:returns: the previous focusable element
|
|
|
|
.. js:function:: getRect(node[, options])
|
|
|
|
Returns the bounding `DOMRect` of a given node (or an empty one if none is given).
|
|
This helper is a bit different than the native `Element.getBoundingClientRect`:
|
|
|
|
- rects take their positions relative to the top window element (instead of their
|
|
parent `<iframe>` if any);
|
|
- they can be trimmed to remove padding with the `trimPadding` option.
|
|
|
|
:returns: the bounding `DOMRect` of the given node
|
|
|
|
.. js:function:: isDisplayed(node)
|
|
|
|
Checks whether a target is displayed, meaning that it has an offset parent and is contained in
|
|
the current document.
|
|
|
|
Note that it does not mean that the target is "visible" (it can still be hidden by CSS
|
|
properties such as `width`, `opacity`, `visiblity`, etc.).
|
|
|
|
:returns: whether the target is displayed
|
|
|
|
.. js:function:: isEditable(node)
|
|
|
|
Returns whether the given node is editable, meaning that it is an `:enabled` `<input>` or
|
|
`<textarea>` `Element`.
|
|
|
|
:returns: whether the target is editable
|
|
|
|
.. js:function:: isEventTarget(node)
|
|
|
|
Returns whether the given target is an `EventTarget`.
|
|
|
|
:returns: whether the target is an event target
|
|
|
|
.. js:function:: isFocusable(node)
|
|
|
|
Returns whether an element is focusable. Focusable elements are either:
|
|
|
|
- `<a>` or `<area>` elements with an `href` attribute;
|
|
- *enabled* `<button>`, `<input>`, `<select>` and `<textarea>` elements;
|
|
- `<iframe>` elements;
|
|
- any element with its `contenteditable` attribute set to `"true"`.
|
|
|
|
A focusable element must also not have a `tabIndex` property set to less than 0.
|
|
|
|
:returns: whether the target is focusable
|
|
|
|
.. js:function:: isInDOM(target)
|
|
|
|
Returns whether the given target is contained in the current root document.
|
|
|
|
:returns: whether the target is in the DOM
|
|
|
|
.. js:function:: isVisible(target)
|
|
|
|
Checks whether an target is visible, meaning that it is "displayed" (see `isDisplayed`), has a
|
|
non-zero width and height, and is not hidden by "opacity" or "visibility" CSS properties.
|
|
|
|
Note that it does not account for:
|
|
|
|
- the position of the target in the viewport (e.g. negative x/y coordinates)
|
|
- the color of the target (e.g. transparent text with no background).
|
|
|
|
:returns: whether the target is visible
|
|
|
|
.. js:function:: matches(node, selector)
|
|
|
|
Returns whether the given node matches the given selector.
|
|
|
|
:returns: whether the node matches the selector
|
|
|
|
.. js:function:: observe(target, callback)
|
|
|
|
Listens for DOM mutations on a given target.
|
|
|
|
This helper has 2 main advantages over directly calling the native `MutationObserver`:
|
|
|
|
- it ensures a single observer is created for a given target, even if multiple callbacks are
|
|
registered;
|
|
|
|
- it keeps track of these observers, which allows to check whether an observer is still running
|
|
while it should not, and to disconnect all running observers at once.
|
|
|
|
.. _hoot/query-all:
|
|
|
|
.. js:function:: queryAll(target[, options])
|
|
|
|
Returns a list of nodes matching the given `Target`.
|
|
This function can either be used as a **template literal tag** (only supports string selector
|
|
without options) or invoked the usual way.
|
|
|
|
The target can be:
|
|
|
|
- a `Node` (or an iterable of nodes), or `Window` object;
|
|
- a `Document` object (which will be converted to its body);
|
|
- a string representing a :ref:`custom selector <hoot/custom-dom-selectors>`
|
|
(which will be queried from the `root` option).
|
|
|
|
An `options` object can be specified to filter[1] the results:
|
|
|
|
- `displayed`: whether the nodes must be "displayed" (see `isDisplayed`);
|
|
- `exact`: the exact number of nodes to match (throws an error if the number of nodes
|
|
doesn't match);
|
|
- `focusable`: whether the nodes must be "focusable" (see `isFocusable`);
|
|
- `root`: the root node to query the selector in (defaults to the current fixture);
|
|
- `visible`: whether the nodes must be "visible" (see `isVisible`).
|
|
* This option implies `displayed`
|
|
|
|
[1] these filters (except for `exact` and `root`) achieve the same result as using their homonym
|
|
pseudo-classes on the final group of the given selector string, e.g.:
|
|
|
|
.. code-block:: javascript
|
|
|
|
// These 2 will return the same result
|
|
queryAll`ul > li:visible`;
|
|
queryAll("ul > li", { visible: true });
|
|
|
|
:returns: a list of nodes
|
|
|
|
.. js:function:: queryAllAttributes(target, attribute[, options])
|
|
|
|
Performs a :ref:`queryAll <hoot/query-all>` on the given `target` and returns
|
|
a list of attribute values.
|
|
|
|
:returns: a list of attribute values
|
|
|
|
.. js:function:: queryAllProperties(target, property[, options])
|
|
|
|
Performs a :ref:`queryAll <hoot/query-all>` on the given `target` and returns
|
|
a list of property values.
|
|
|
|
:returns: a list of property values
|
|
|
|
.. js:function:: queryAllTexts(target[, options])
|
|
|
|
Performs a :ref:`queryAll <hoot/query-all>` on the given `target` and returns
|
|
a list of text contents.
|
|
|
|
:returns: a list of text contents
|
|
|
|
.. js:function:: queryAllValues(target[, options])
|
|
|
|
Performs a :ref:`queryAll <hoot/query-all>` on the given `target` and returns
|
|
a list of values.
|
|
|
|
:returns: a list of values
|
|
|
|
.. js:function:: queryAttribute(target, attribute[, options])
|
|
|
|
Performs a :ref:`queryOne <hoot/query-one>` with the given arguments and returns
|
|
the value of the given `attribute` of the matching node.
|
|
|
|
:returns: the first attribute value
|
|
|
|
.. js:function:: queryFirst(target[, options])
|
|
|
|
Performs a :ref:`queryAll <hoot/query-all>` with the given arguments and returns
|
|
the first result or `null`.
|
|
|
|
:returns: the first matching node
|
|
|
|
.. _hoot/query-one:
|
|
|
|
.. js:function:: queryOne(target[, options])
|
|
|
|
Performs a :ref:`queryAll <hoot/query-all>` with the given arguments, along with
|
|
a forced `exact: 1` option to ensure only one node matches the given `Target`.
|
|
|
|
The returned value is a single node instead of a list of nodes.
|
|
|
|
:returns: a single node
|
|
|
|
.. js:function:: queryText(target[, options])
|
|
|
|
Performs a :ref:`queryOne <hoot/query-one>` with the given arguments and returns
|
|
the *text* of the matching node.
|
|
|
|
:returns: the text of the matching node
|
|
|
|
.. js:function:: queryValue(target[, options])
|
|
|
|
Performs a :ref:`queryOne <hoot/query-one>` with the given arguments and returns
|
|
the *value* of the matching node.
|
|
|
|
:returns: the value of the matching node
|
|
|
|
.. _hoot/wait-for:
|
|
|
|
.. js:function:: waitFor(target[, options])
|
|
|
|
Combination of :ref:`queryAll <hoot/query-all>` and :ref:`waitUntil <hoot/wait-until>`:
|
|
waits for a given target to match elements in the DOM and returns the first
|
|
matching node when it appears (or immediatlly if it is already present).
|
|
|
|
:returns: a promise of the first matching node
|
|
|
|
.. js:function:: waitForNone(target[, options])
|
|
|
|
Opposite of :ref:`waitFor <hoot/wait-for>`: waits for a given target to disappear from the DOM.
|
|
|
|
:returns: a promise of the number of matching nodes
|
|
|
|
.. js:function:: watchKeys(target, whiteList)
|
|
|
|
Returns a function checking that the given target does not contain any unexpected key. The list
|
|
of accepted keys is the initial list of keys of the target, along with an optional `whiteList`
|
|
argument.
|
|
|
|
:returns: a function checking that the target does not contain any unexpected key
|
|
|
|
|
|
Interaction helpers
|
|
===================
|
|
|
|
.. js:function:: check(target[, options])
|
|
|
|
Ensures that the given `Target` is checked.
|
|
|
|
If it is not checked, a :ref:`click <hoot/click>` is simulated on the input.
|
|
If the input is still not checked after the click, an error is thrown.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
check("input[type=checkbox]"); // Checks the first <input> checkbox element
|
|
|
|
.. _hoot/clear:
|
|
|
|
.. js:function:: clear([options])
|
|
|
|
Clears the **value** of the current **active element**.
|
|
|
|
This is done using the following sequence:
|
|
|
|
- pressing `"Control"` & `"A"` to select the whole value;
|
|
- pressing `"Backspace"` to delete the value;
|
|
- (optional) triggering a `"change"` event by pressing `"Enter"`.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
clear(); // Clears the value of the current active element
|
|
|
|
.. _hoot/click:
|
|
|
|
.. js:function:: click(target[, options])
|
|
|
|
Performs a click sequence on the given `Target`.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `pointerdown`
|
|
- [desktop] `mousedown`
|
|
- [touch] `touchstart`
|
|
- [target is not active element] `blur`
|
|
- [target is focusable] `focus`
|
|
- `pointerup`
|
|
- [desktop] `mouseup`
|
|
- [touch] `touchend`
|
|
- `click`
|
|
- `dblclick` if click is not prevented & current click count is even
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
click("button"); // Clicks on the first <button> element
|
|
|
|
.. js:function:: dblclick(target[, options])
|
|
|
|
Performs two :ref:`click <hoot/click>` sequences on the given `Target`.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
dblclick("button"); // Double-clicks on the first <button> element
|
|
|
|
.. js:function:: drag(target[, options])
|
|
|
|
Starts a drag sequence on the given `Target`.
|
|
|
|
Returns a set of helper functions to direct the sequence:
|
|
|
|
- `moveTo`: moves the pointer to the given target;
|
|
- `drop`: drops the dragged element on the given target (if any);
|
|
- `cancel`: cancels the drag sequence.
|
|
|
|
:returns: `Promise<DragHelpers>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
drag(".card:first").drop(".card:last"); // Drags the first card onto the last one
|
|
|
|
drag(".card:first").moveTo(".card:last").drop(); // Same as above
|
|
|
|
const { cancel, moveTo } = await drag(".card:first"); // Starts the drag sequence
|
|
moveTo(".card:eq(3)"); // Moves the dragged card to the 4th card
|
|
cancel(); // Cancels the drag sequence
|
|
|
|
.. js:function:: edit(value[, options])
|
|
|
|
Combination of :ref:`clear <hoot/clear>` and :ref:`fill <hoot/fill>`:
|
|
|
|
- first, clears the input value (if any)
|
|
- then fills the input with the given value
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
fill("foo"); // Types "foo" in the active element
|
|
edit("Hello World"); // Replaces "foo" by "Hello World"
|
|
|
|
.. _hoot/fill:
|
|
|
|
.. js:function:: fill(value[, options])
|
|
|
|
Fills the current **active element** with the given `value`. This helper is intended
|
|
for `<input>` and `<textarea>` elements, with the exception of `"checkbox"` and
|
|
`"radio"` types, which should be selected using the `check` helper.
|
|
|
|
If the target is an editable input, its string `value` will be input one character
|
|
at a time, each generating its corresponding keyboard event sequence. This behavior
|
|
can be overriden by passing the `instantly` option, which will instead simulate
|
|
a `control` + `v` keyboard sequence, resulting in the whole text being pasted.
|
|
|
|
Note that the given value is **appended** to the current value of the element.
|
|
|
|
If the active element is a `<input type="file"/>`, the `value` should be a
|
|
`File`/list of `File` object(s).
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
fill("Hello World"); // Types "Hello World" in the active element
|
|
fill("Hello World", { instantly: true }); // Pastes "Hello World" in the active element
|
|
fill(new File(["Hello World"], "hello.txt")); // Uploads a file named "hello.txt" with "Hello World" as content
|
|
|
|
.. js:function:: hover(target[, options])
|
|
|
|
Performs a hover sequence on the given `Target`.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `pointerover`
|
|
- [desktop] `mouseover`
|
|
- `pointerenter`
|
|
- [desktop] `mouseenter`
|
|
- `pointermove`
|
|
- [desktop] `mousemove`
|
|
- [touch] `touchmove`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
hover("button"); // Hovers the first <button> element
|
|
|
|
.. js:function:: keyDown(keyStrokes[, options])
|
|
|
|
Performs a key down sequence on the current **active element**.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `keydown`
|
|
|
|
Additional actions will be performed depending on the key pressed:
|
|
|
|
- `Tab`: focus next (or previous with `shift`) focusable element;
|
|
- `c`: copy current selection to clipboard;
|
|
- `v`: paste current clipboard content to current element;
|
|
- `Enter`: submit the form if the target is a `<button type="button">` or
|
|
a `<form>` element, or trigger a `change` event on the target if it is
|
|
an `<input>` element;
|
|
- `Space`: trigger a `click` event on the target if it is an `<input type="checkbox">`
|
|
element.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
keyDown(" "); // Space key
|
|
|
|
.. js:function:: keyUp(keyStrokes[, options])
|
|
|
|
Performs a key up sequence on the current **active element**.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `keyup`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
keyUp("Enter");
|
|
|
|
.. js:function:: leave([options])
|
|
|
|
Performs a leave sequence on the current **window**.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `pointermove`
|
|
- [desktop] `mousemove`
|
|
- [touch] `touchmove`
|
|
- `pointerout`
|
|
- [desktop] `mouseout`
|
|
- `pointerleave`
|
|
- [desktop] `mouseleave`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
leave("button"); // Moves out of <button>
|
|
|
|
.. js:function:: on(target, type[, listener[, options]])
|
|
|
|
Shorthand helper to attach an event listener to the given `Target`, and
|
|
returning a function to remove the listener.
|
|
|
|
:returns: `off` function to de-bind the bound listener
|
|
|
|
.. code-block:: javascript
|
|
|
|
const off = on("button", "click", onClick);
|
|
after(off);
|
|
|
|
.. js:function:: pointerDown(target[, options])
|
|
|
|
Performs a pointer down on the given `Target`.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `pointerdown`
|
|
- [desktop] `mousedown`
|
|
- [touch] `touchstart`
|
|
- [target is not active element] `blur`
|
|
- [target is focusable] `focus`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
pointerDown("button"); // Focuses to the first <button> element
|
|
|
|
.. js:function:: pointerUp(target[, options])
|
|
|
|
Performs a pointer up on the given `Target`.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `pointerup`
|
|
- [desktop] `mouseup`
|
|
- [touch] `touchend`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
pointerUp("body"); // Triggers a pointer up on the <body> element
|
|
|
|
.. _hoot/press:
|
|
|
|
.. js:function:: press(keyStrokes[, options])
|
|
|
|
Performs a keyboard event sequence on the current **active element**.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `keydown`
|
|
- `keyup`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
pointerDown("button[type=submit]"); // Moves focus to <button>
|
|
keyDown("Enter"); // Submits the form
|
|
|
|
keyDown("Shift+Tab"); // Focuses previous focusable element
|
|
|
|
keyDown(["ctrl", "v"]); // Pastes current clipboard content
|
|
|
|
.. js:function:: resize([dimensions[, options]])
|
|
|
|
Performs a resize event sequence on the current **window**.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `resize`
|
|
|
|
The target will be resized to the given dimensions, enforced by `!important` style
|
|
attributes.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
resize("body", { width: 1000, height: 500 }); // Resizes <body> to 1000x500
|
|
|
|
.. js:function:: scroll(target, position[, options])
|
|
|
|
Performs a scroll event sequence on the given `Target`.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- [desktop] `wheel`
|
|
- `scroll`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
scroll("body", { y: 0 }); // Scrolls to the top of <body>
|
|
|
|
.. js:function:: select(value[, options])
|
|
|
|
Performs a selection event sequence current active element. This helper is intended
|
|
for `<select>` elements only.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `change`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
click("select[name=country]"); // Focuses <select> element
|
|
select("belgium"); // Selects the <option value="belgium"> element
|
|
|
|
.. js:function:: setInputFiles(files[, options])
|
|
|
|
Gives the given `File` list to the current file input. This helper only
|
|
works if a file input has been previously interacted with (by clicking on it).
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. js:function:: setInputRange(target, value[, options])
|
|
|
|
Sets the given value to the current "input[type=range]" `Target`.
|
|
|
|
The event sequence is as follows:
|
|
|
|
- `pointerdown`
|
|
- `input`
|
|
- `change`
|
|
- `pointerup`
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. js:function:: uncheck(target[, options])
|
|
|
|
Ensures that the given `Target` is unchecked.
|
|
|
|
If it is checked, a :ref:`click <hoot/click>` is triggered on the input.
|
|
If the input is still checked after the click, an error is thrown.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
.. code-block:: javascript
|
|
|
|
uncheck("input[type=checkbox]"); // Unchecks the first <input> checkbox element
|
|
|
|
.. js:function:: unload([options])
|
|
|
|
Triggers a "beforeunload" event the current **window**.
|
|
|
|
:returns: `Promise<Event[]>`
|
|
|
|
|
|
Mocks
|
|
=====
|
|
|
|
By default, a lot of low-level features are mocked by Hoot: `clipboard`, `fetch`, `localStorage`,
|
|
etc. These mocks are intended to not produce any side-effect that would disturb the test runner
|
|
or the context of other tests, while still providing the same interface to allow tests to rely
|
|
on these features seemlessly.
|
|
|
|
There is also a need (most of the time) to force actions on these features or change their
|
|
behavior for a test, so there exist helpers to interact with these mocked features. The following
|
|
sections will list the main mocked features and the means to interact with them.
|
|
|
|
Time
|
|
----
|
|
|
|
Most asynchronous features are mocked: "timers" (`setTimeout`, `setInterval` and
|
|
`requestAnimationFrame`), `Date` and `performance` all behave normally, but can be canceled or
|
|
sped-up manually to considerably shorten the actual duration of tests. For example: all "timers"
|
|
are canceled at the end of each test to avoid side-effects for the next one.
|
|
|
|
.. important::
|
|
There are 2 main timing behaviors that are *NOT* mocked:
|
|
|
|
- `Promise` objects and related API;
|
|
- OWL's timer functions: to wait for OWL rendering functions, you'll have
|
|
to resort to the `animationFrame` helper.
|
|
|
|
Related helpers
|
|
~~~~~~~~~~~~~~~
|
|
|
|
.. js:function:: advanceFrame(frameCount)
|
|
|
|
Calls `advanceTime` with the duration it would take for a given `frameCount`
|
|
amount of frames to have rendered in the UI (i.e. (1000 / current FPS) x frame count).
|
|
|
|
.. js:function:: advanceTime(ms)
|
|
|
|
Advances the current time by the given amount of milliseconds. This will
|
|
affect all timeouts, intervals, animations and date objects.
|
|
|
|
It returns a promise resolved after all related callbacks have been executed.
|
|
|
|
:returns: time consumed by timers (in ms)
|
|
|
|
.. js:function:: animationFrame()
|
|
|
|
Returns a promise resolved after the next animation frame, typically allowing
|
|
Owl components to render.
|
|
|
|
:returns: `Deferred`
|
|
|
|
.. js:function:: cancelAllTimers()
|
|
|
|
Cancels all current timeouts, intervals and animations.
|
|
|
|
.. js:function:: delay()
|
|
|
|
Returns a promise resolved after a given amount of milliseconds (default to **0**).
|
|
|
|
.. code-block:: javascript
|
|
|
|
await delay(1000); // waits for 1 second
|
|
|
|
:returns: `Deferred`
|
|
|
|
.. js:function:: microTick()
|
|
|
|
Returns a promise resolved after the next microtask tick.
|
|
|
|
:returns: `Promise`
|
|
|
|
.. js:function:: mockDate(date[, tz])
|
|
|
|
Mocks the current date and time, and also the time zone if any.
|
|
|
|
Date can either be an object describing the date and time to mock, or a
|
|
string in SQL or ISO format (time and millisecond values can be omitted).
|
|
See :ref:`mockTimeZone <hoot/mock-timezone>` for the time zone params.
|
|
|
|
.. code-block:: javascript
|
|
|
|
mockDate("2023-12-25T20:45:00"); // 2023-12-25 20:45:00 UTC
|
|
mockDate({ year: 2023, month: 12, day: 25, hour: 20, minute: 45 }); // same as above
|
|
mockDate("2019-02-11 09:30:00.001", +2);
|
|
|
|
.. _hoot/mock-timezone:
|
|
|
|
.. js:function:: mockTimeZone(tz)
|
|
|
|
Mocks the current time zone.
|
|
|
|
Time zone can either be a locale, a time zone or an offset.
|
|
|
|
Returns a function restoring the default zone.
|
|
|
|
.. code-block:: javascript
|
|
|
|
mockTimeZone(+1); // UTC + 1
|
|
mockTimeZone("Europe/Brussels"); // UTC + 1 (or UTC + 2 in summer)
|
|
mockTimeZone("ja-JP"); // UTC + 9
|
|
|
|
.. js:function:: runAllTimers([preventTimers])
|
|
|
|
Calculates the amount of time needed to run all current timeouts, intervals and
|
|
animations, and then advances the current time by that amount.
|
|
|
|
:returns: `Promise`
|
|
|
|
.. js:function:: setFrameRate(frameRate)
|
|
|
|
Sets the current frame rate (in fps) used by animation frames (default to 60fps).
|
|
|
|
.. js:function:: tick()
|
|
|
|
Returns a promise resolved after the next task tick.
|
|
|
|
:returns: `Deferred`
|
|
|
|
.. _hoot/wait-until:
|
|
|
|
.. js:function:: waitUntil(predicate[, options])
|
|
|
|
Returns a promise fulfilled when the given `predicate` returns a truthy value, with the value of
|
|
the promise being the return value of the `predicate`.
|
|
|
|
The `predicate` is run once initially and then each time the DOM is mutated (see `observe` for
|
|
more information).
|
|
|
|
The promise automatically rejects after a given `timeout` (defaults to 5 seconds).
|
|
|
|
:returns: a promise of the return value of the predicate
|
|
|
|
.. js:class:: Deferred()
|
|
|
|
Manually resolvable and rejectable promise
|
|
|
|
.. js:method:: reject(value)
|
|
|
|
Rejects the deferred with the given reason
|
|
|
|
.. js:method:: resolve(value)
|
|
|
|
Resolves the deferred with the given value
|
|
|
|
Network
|
|
-------
|
|
|
|
In general, we don't want to perform actual network calls in tests. To ensure this, all calls
|
|
to `fetch` and `XMLHttpRequest` have been re-routed to a function given to
|
|
:ref:`mockFetch <hoot/mock-fetch>`.
|
|
|
|
.. note::
|
|
In Odoo, this is generally implicitly handled by a :ref:`MockServer <mock-server/mock-server>`
|
|
which is spawned by the mock environment, i.e. any time a component is rendered using the
|
|
:ref:`mountWithCleanup <web-test-helpers/mount-with-cleanup>` helper.
|
|
|
|
Related helpers
|
|
~~~~~~~~~~~~~~~
|
|
|
|
.. _hoot/mock-fetch:
|
|
|
|
.. js:function:: mockFetch([fetchFn])
|
|
|
|
Mocks the fetch function by replacing it with a given `fetchFn`.
|
|
|
|
The return value of `fetchFn` is used as the response of the mocked fetch, or
|
|
wrapped in a `MockResponse` object if it does not meet the required format.
|
|
|
|
.. code-block:: javascript
|
|
|
|
mockFetch((input, init) => {
|
|
if (input === "/../web_search_read") {
|
|
return { records: [{ id: 3, name: "john" }] };
|
|
}
|
|
// ...
|
|
});
|
|
mockFetch((input, init) => {
|
|
if (input === "/translations") {
|
|
const translations = {
|
|
"Hello, world!": "Bonjour, monde !",
|
|
// ...
|
|
};
|
|
return new Response(JSON.stringify(translations));
|
|
}
|
|
});
|
|
|
|
.. _hoot/mock-websocket:
|
|
|
|
.. js:function:: mockWebSocket([onWebSocketConnected])
|
|
|
|
Activates mock WebSocket classe:
|
|
|
|
- websocket connections will be handled by `window.fetch`
|
|
(see :ref:`mockFetch <hoot/mock-fetch>`);
|
|
- the `onWebSocketConnected` callback will be called after a websocket has been created.
|
|
|
|
.. js:function:: mockWorker([onWorkerConnected])
|
|
|
|
Activates mock `Worker` and `SharedWorker` classes:
|
|
|
|
- actual code fetched by worker URLs will then be handled by `window.fetch`
|
|
(see :ref:`mockFetch <hoot/mock-fetch>`);
|
|
- the `onWorkerConnected` callback will be called after a worker has been created.
|
|
|
|
Notable global features
|
|
-----------------------
|
|
|
|
The following features may not have any specific mocked feature added, but they do work as
|
|
expected without changing the actual properties they were meant to:
|
|
|
|
#. Document
|
|
|
|
Both `title` and `cookie` can be set and read without changing the actual properties
|
|
of the current document.
|
|
|
|
#. History
|
|
|
|
The `history` API is mocked and bound to the `mockLocation` object to return the same
|
|
values and provide consistency.
|
|
|
|
#. Location
|
|
|
|
Hoot returns a `mockLocation` object to use instead of `window.location`, but this relies on
|
|
the use of an indirection in the actual production code.
|
|
|
|
.. important::
|
|
This feature will only work if an indirection is set between production code and
|
|
calls to `window.location`. In Odoo, it works because the `@web/core/browser` module
|
|
provides such an indirection, and that module is mocked in test environments to redirect
|
|
to the `mockLocation` object.
|
|
|
|
#. Navigator
|
|
|
|
Most used navigator features, such as the `clipboard` API and `userAgent`, have
|
|
been mocked to hijack their actual behaviors. Its `permissions` object has been bound
|
|
to a global mock of the permissions API.
|
|
|
|
#. Notification
|
|
|
|
Notifications have been mocked, with the "notification" permissions bound to the global
|
|
mocked permissions API.
|
|
|
|
#. Storage
|
|
|
|
`localStorage` and `sessionStorage` both point to "virtual" storages.
|
|
|
|
#. Touch
|
|
|
|
Touch features can be force-activated or deactivated globally for a given test/suite
|
|
using the :ref:`mockTouch <hoot/mock-touch>` helper. It will mock both the presence
|
|
of touch handlers like `ontouchstart` on window, as well as the `"pointer"` media
|
|
being set to `fine` or `coarse`.
|
|
|
|
Related helpers
|
|
~~~~~~~~~~~~~~~
|
|
|
|
.. js:function:: flushNotifications()
|
|
|
|
Returns the list of notifications that have been created since the last call
|
|
to this function, consuming it in the process.
|
|
|
|
.. js:function:: mockPermission(name[, value])
|
|
|
|
.. _hoot/mock-touch:
|
|
|
|
.. js:function:: mockTouch(setTouch)
|