HOOT¶
Overview¶
HOOT 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 lib/
in the Odoo codebase and exports 2 main modules:
@odoo/hoot-dom
: (can be used in tours) helpers to:query elements from the DOM, such as
queryAll()
andwaitFor()
;
@odoo/hoot
: (only to be used in unit tests) all the test framework features:test
,describe
andexpect
test hooks like
after
andafterEach
fixture handling with
getFixture
date and time handling like
mockDate
oradvanceTime
mocking network responses through
mockFetch()
ormockWebSocket()
every helper exported by
@odoo/hoot-dom
Nota
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 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
start.hoot.js
file takes care of calling the test runner with its exportedstart
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. SeeTestRunner.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 logged2
: all test results are also logged3
: 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)
Nota
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:
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:
import { describe, expect, test } from "@odoo/hoot";
describe("My first suite", () => {
test("My first test", () => {
expect(2 + 2).toBe(4);
});
});
Importante
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.
expect(true).not.toBe(false);
resolves
Waits for the value (
Promise
) to be “resolved” before running the following matcher with the resolved value.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.await expect(Promise.reject("error")).rejects.toBe("error");
Nota
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:
- toBe(expected[, options])¶
Expects the received value to be strictly equal to the
expected
value.Parameters
expected
:any
options
:{ message?: string }
Examples
expect("foo").toBe("foo"); expect({ foo: 1 }).not.toBe({ foo: 1 });
- 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
expect(0.2 + 0.1).toBeCloseTo(0.3); expect(3.51).toBeCloseTo(3.5, { digits: 1 });
- toBeEmpty([options])¶
Expects the received value to be empty:
iterable
: no itemsobject
: no keysnode
: no content (i.e. no value or text)anything else: falsy value (
false
,0
,""
,null
,undefined
)
Parameters
options
:{ message?: string }
Examples
expect({}).toBeEmpty(); expect(["a", "b"]).not.toBeEmpty(); expect(queryOne("input")).toBeEmpty();
- toBeGreaterThan(min[, options])¶
Expects the received value to be strictly greater than
min
.Parameters
min
:number
options
:{ message?: string }
Examples
expect(5).toBeGreaterThan(-1); expect(4 + 2).toBeGreaterThan(5);
- toBeInstanceOf(cls[, options])¶
Expects the received value to be an instance of the given
cls
.Parameters
cls
:Function
options
:{ message?: string }
Examples
expect({ foo: 1 }).not.toBeInstanceOf(Object); expect(document.createElement("div")).toBeInstanceOf(HTMLElement);
- toBeLessThan(max[, options])¶
Expects the received value to be strictly less than
max
.Parameters
max
:number
options
:{ message?: string }
Examples
expect(5).toBeLessThan(10); expect(8 - 6).toBeLessThan(3);
- toBeOfType(type[, options])¶
Expects the received value to be of the given
type
.Parameters
type
:string
options
:{ message?: string }
Examples
expect("foo").toBeOfType("string"); expect({ foo: 1 }).toBeOfType("object");
- toBeWithin(min, max[, options])¶
Expects the received value to be between
min
andmax
(both inclusive).Parameters
min
:number
max
:number
options
:{ message?: string }
Examples
expect(3).toBeWithin(3, 9); expect(-8.5).toBeWithin(-20, 0); expect(100).toBeWithin(50, 100);
- toEqual(expected[, options])¶
Expects the received value to be deeply equal to the
expected
value.Parameters
expected
:any
options
:{ message?: string }
Examples
expect(["foo"]).toEqual(["foo"]); expect({ foo: 1 }).toEqual({ foo: 1 });
- toHaveLength(length[, options])¶
Expects the received value to have a length of the given
length
. Received value can be anyIterable
orObject
.Parameters
length
:number
options
:{ message?: string }
Examples
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(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
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(matcher[, options])¶
Expects the received value to match the given
matcher
.Parameters
matcher
:string | number | RegExp
options
:{ message?: string }
Examples
expect(new Error("foo")).toMatch("foo"); expect("a foo value").toMatch(/fo.*ue/);
- toThrow(matcher[, options])¶
Expects the received
Function
to throw an error after being called.Parameters
matcher
:string | number | RegExp
options
:{ message?: string }
Examples
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 custom selector
as the argument of the expect
function (although a Node
or an iterable of Node
is also accepted).
- toBeChecked([options])¶
Expects the received
Target
to be"checked"
, or to be"indeterminate"
if the homonymous option is set totrue
.Parameters
options
:{ message?: string, indeterminate?: boolean }
Examples
expect("input[type=checkbox]").toBeChecked();
- 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
expect(document.body).toBeDisplayed(); expect(document.createElement("div")).not.toBeDisplayed();
- toBeEnabled([options])¶
Expects the received
Target
to be “enabled”, meaning that it matches the:enabled
pseudo-selector.Parameters
options
:{ message?: string }
Examples
expect("button").toBeEnabled(); expect("input[type=radio]").not.toBeEnabled();
- toBeFocused([options])¶
Expects the received
Target
to be “focused” in its owner document.Parameters
options
:{ message?: string }
- 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
expect(document.body).toBeVisible(); expect("[style='opacity: 0']").not.toBeVisible();
- toHaveAttribute(attribute, value[, options])¶
Expects the received
Target
to have the given attribute set, and for that attribute value to match the givenvalue
if any.Parameters
attribute
:string
value
:string | number | RegExp
options
:{ message?: string }
Examples
expect("a").toHaveAttribute("href"); expect("script").toHaveAttribute("src", "./index.js");
- toHaveClass(className[, options])¶
Expects the received
Target
to have the given class name(s).Parameters
className
:string | string[]
options
:{ message?: string }
Examples
expect("button").toHaveClass("btn btn-primary"); expect("body").toHaveClass(["o_webclient", "o_dark"]);
- toHaveCount(amount[, options])¶
Expects the received
Target
to contain exactlyamount
element(s). Note that theamount
parameter can be omitted, in which case the function will expect at least one element.Parameters
amount
:number
options
:{ message?: string }
Examples
expect(".o_webclient").toHaveCount(1); expect(".o_form_view .o_field_widget").toHaveCount(); expect("ul > li").toHaveCount(4);
- toHaveInnerHTML(expected[, options])¶
Expects the
innerHTML
of the receivedTarget
to match theexpected
value (upon formatting).Parameters
expected
:string | RegExp
options
:{ message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }
Examples
expect(".my_element").toHaveInnerHTML(` Some <strong>text</strong> `);
- toHaveOuterHTML(expected[, options])¶
Expects the
outerHTML
of the receivedTarget
to match theexpected
value (upon formatting).Parameters
expected
:string | RegExp
options
:{ message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }
Examples
expect(".my_element").toHaveOuterHTML(` <div class="my_element"> Some <strong>text</strong> </div> `);
- toHaveProperty(property, value[, options])¶
Expects the received
Target
to have its given property value match the givenvalue
. 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
expect("button").toHaveProperty("tabIndex", 0); expect("input").toHaveProperty("ontouchstart"); expect("script").toHaveProperty("src", "./index.js");
- toHaveRect(rect[, options])¶
Expects the
DOMRect
of the receivedTarget
to match the givenrect
object. Therect
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
expect("button").toHaveRect({ x: 20, width: 100, height: 50 }); expect("button").toHaveRect(".container");
- toHaveStyle(style[, options])¶
Expects the received
Target
to match the given style properties.Parameters
style
:string | Record<string, string | RegExp>
options
:{ message?: string }
Examples
expect("button").toHaveStyle({ color: "red" }); expect("p").toHaveStyle("text-align: center");
- toHaveText(text[, options])¶
Expects the
text
content of the receivedTarget
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
expect("p").toHaveText("lorem ipsum dolor sit amet"); expect("header h1").toHaveText(/odoo/i);
- 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
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.
- expect.assertions(expected)¶
- Argumentos
expected (
number()
) –
Expects the current test to have the
expected
amount of assertions. This number cannot be less than 1.Nota
It is generally preferred to use
expect.step()
andexpect.verifySteps()
instead as it is more reliable and allows to test more extensively.
- expect.errors(expected)¶
- Argumentos
expected (
number()
) –
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.
- expect.step(value)¶
- Argumentos
value (
unknown()
) –
Registers a step for the current test, that can be consumed by
expect.verifySteps()
. Unconsumed steps will fail the test.
- expect.verifyErrors(errors[, options])¶
- Argumentos
errors (
unknown[]()
) –options (
{ message?: string }()
) –
- Retorna
boolean
Expects the received matchers to match the errors thrown since the start of the test or the last call to
expect.verifyErrors()
. Calling this matcher will reset the list of current errors.expect.verifyErrors([/RPCError/, /Invalid domain AST/]);
- expect.verifySteps(steps[, options])¶
- Argumentos
steps (
unknown[]()
) –options (
{ ignoreOrder?: boolean, message?: string, partial?: boolean }()
) –
- Retorna
boolean
Expects the received steps to be equal to the steps emitted since the start of the test or the last call to
expect.verifySteps()
. Calling this matcher will reset the list of current steps.expect.step("web_read_group"); expect.step([1, 2]); expect.verifySteps(["web_read_group", [1, 2]]);
- expect.waitForErrors(errors[, options])¶
- Argumentos
errors (
unknown[]()
) –options (
{ message?: string }()
) –
- Retorna
Promise<boolean>
Same as
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.
fetch("invalid/url"); await expect.waitForErrors([/RPCError/]);
- expect.waitForSteps(steps[, options])¶
- Argumentos
steps (
unknown[]()
) –options (
{ ignoreOrder?: boolean, message?: string, partial?: boolean }()
) –
- Retorna
Promise<boolean>
Same as
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.
// ... step on each 'web_read_group' call fetch(".../call_kw/web_read_group"); await expect.waitForSteps(["web_read_group"]);
DOM: queries¶
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 thei
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
<iframe>
elements, and returns theirbody
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 thei
flag at the end of the regex)
:visible
matches nodes that are “visible” (see
isVisible
)
Query & node properties helpers¶
Hoot provides helpers to query nodes and some of their properties in a streamlined
and elegant way. This can mainly be done through the use of queryX
helpers:
- 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), orWindow
object;a
Document
object (which will be converted to its body);a string representing a custom selector (which will be queried from the
root
option).
An
options
object can be specified to filter 1 the results:count
: the exact number of nodes to match (throws an error if the number of nodes doesn’t match);displayed
: whether the nodes must be “displayed” (seeisDisplayed
);focusable
: whether the nodes must be “focusable” (seeisFocusable
);root
: the root node to query the selector in (defaults to the current fixture);visible
: whether the nodes must be “visible” (seeisVisible
). * This option impliesdisplayed
- 1
these filters (except for
count
androot
) achieve the same result as using their homonym pseudo-classes on the final group of the given selector string, e.g.:// These 2 will return the same result queryAll`ul > li:visible`; queryAll("ul > li", { visible: true });
- Retorna
Node[]
- queryAllAttributes(target, attribute[, options])¶
Performs a
queryAll()
on the giventarget
and returns a list of attribute values.- Retorna
string[]
list of attribute values
- queryAllProperties(target, property[, options])¶
Performs a
queryAll()
on the giventarget
and returns a list of property values.- Retorna
unknown[]
list of property values
- queryAllTexts(target[, options])¶
Performs a
queryAll()
on the giventarget
and returns a list of text contents.- Retorna
string[]
list of text contents
- queryAllValues(target[, options])¶
Performs a
queryAll()
on the giventarget
and returns a list of values.- Retorna
string[]
a list of values
- queryAttribute(target, attribute[, options])¶
Performs a
queryOne()
with the given arguments and returns the value of the givenattribute
of the matching node.- Retorna
string
the attribute value
- queryFirst(target[, options])¶
Performs a
queryAll()
with the given arguments and returns the first result ornull
.- Retorna
Node
|null
the first matching node
- queryOne(target[, options])¶
Performs a
queryAll()
with the given arguments, along with a forcedcount: 1
option to ensure only one node matches the givenTarget
.The returned value is a single node instead of a list of nodes.
- Retorna
Node
a single node
- queryText(target[, options])¶
Performs a
queryOne()
with the given arguments and returns the text of the matching node.- Retorna
string
text of the matching node
- queryValue(target[, options])¶
Performs a
queryOne()
with the given arguments and returns the value of the matching node.- Retorna
string
value of the matching node
All of the above helpers are synchronous, meaning that they will attempt to query nodes instantly. Although some use cases require the element to be awaited for an arbitrary amount of time, unknown in advance due to UI fetching and rendering complexity.
Hoot provides 2 methods to wait for an element to appear / disappear within a certain
time frame (by default: 200
milliseconds) for such cases:
- waitFor(target[, options])¶
Combination of
queryAll()
andwaitUntil()
: waits for a given target to match elements in the DOM and returns the first matching node when it appears (or immediately if it is already present).- Retorna
Promise<Node>
containing the first matching node
DOM: interaction helpers¶
Along with querying elements, it is often required to interact with them. As such, Hoot provides helpers to simulate various user interactions on elements.
These can be split into 2 types based on their parameters: pointer-based interaction helpers, and the other ones.
Pointer interaction helpers:¶
Pointer interaction helpers (such as click()
or drag()
) will simulate
actual pointer movements and events on the given target, and on any previous element
the pointer was supposed to have been.
- check(target[, options])¶
Ensures that the given
Target
is checked.If it is not checked, a
click()
is simulated on the input. If the input is still not checked after the click, an error is thrown.- Retorna
Promise<Event[]>
check("input[type=checkbox]"); // Checks the first <input> checkbox element
- 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
- Retorna
Promise<Event[]>
click("button"); // Clicks on the first <button> element
- dblclick(target[, options])¶
Performs two
click()
sequences on the givenTarget
.- Retorna
Promise<Event[]>
dblclick("button"); // Double-clicks on the first <button> element
- 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.
- Retorna
Promise<DragHelpers>
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
- 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
- Retorna
Promise<Event[]>
hover("button"); // Hovers the first <button> element
- 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
- Retorna
Promise<Event[]>
pointerDown("button"); // Focuses to the first <button> element
- pointerUp(target[, options])¶
Performs a pointer up on the given
Target
.The event sequence is as follows:
pointerup
[desktop]
mouseup
[touch]
touchend
- Retorna
Promise<Event[]>
pointerUp("body"); // Triggers a pointer up on the <body> element
- scroll(target, position[, options])¶
Performs a scroll event sequence on the given
Target
.The event sequence is as follows:
[desktop]
wheel
scroll
- Retorna
Promise<Event[]>
scroll("body", { y: 0 }); // Scrolls to the top of <body>
- 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
- Retorna
Promise<Event[]>
Other interaction helpers:¶
Other interaction helpers will not have a target
parameter. It is not needed,
since pressing keys on a keyboard (for example) is done on the current active element.
- 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"
.
- Retorna
Promise<Event[]>
clear(); // Clears the value of the current active element
- edit(value[, options])¶
Combination of
clear()
andfill()
:first, clears the input value (if any)
then fills the input with the given value
- Retorna
Promise<Event[]>
fill("foo"); // Types "foo" in the active element edit("Hello World"); // Replaces "foo" by "Hello World"
- 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 thecheck
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 overridden by passing theinstantly
option, which will instead simulate acontrol
+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"/>
, thevalue
should be aFile
/list ofFile
object(s).- Retorna
Promise<Event[]>
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
- 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 withshift
) 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 achange
event on the target if it is an<input>
element;Space
: trigger aclick
event on the target if it is an<input type="checkbox">
element.
- Retorna
Promise<Event[]>
keyDown(" "); // Space key
- keyUp(keyStrokes[, options])¶
Performs a key up sequence on the current active element.
The event sequence is as follows:
keyup
- Retorna
Promise<Event[]>
keyUp("Enter");
- 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
- Retorna
Promise<Event[]>
leave("button"); // Moves out of <button>
- press(keyStrokes[, options])¶
Performs a keyboard event sequence on the current active element.
The event sequence is as follows:
keydown
keyup
- Retorna
Promise<Event[]>
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
- 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.- Retorna
Promise<Event[]>
resize("body", { width: 1000, height: 500 }); // Resizes <body> to 1000x500
- select(value[, options])¶
Performs a selection event sequence on the current active element. This helper is intended for
<select>
elements only.The event sequence is as follows:
change
- Retorna
Promise<Event[]>
click("select[name=country]"); // Focuses <select> element select("belgium"); // Selects the <option value="belgium"> element
- 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).- Retorna
Promise<Event[]>
- unload([options])¶
Triggers a “beforeunload” event on the current
Window
.- Retorna
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 seamlessly.
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.
Importante
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.
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
mockFetch()
.
Nota
In Odoo, this is generally implicitly handled by a MockServer which is spawned by the mock environment, i.e. any time a component is rendered using the mountWithCleanup helper.
Related helpers¶
- 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 aMockResponse
object if it does not meet the required format.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)); } });
- mockWebSocket([onWebSocketConnected])¶
Activates mock WebSocket classe:
websocket connections will be handled by
window.fetch
(seemockFetch()
);the
onWebSocketConnected
callback will be called after a websocket has been created.
- mockWorker([onWorkerConnected])¶
Activates mock
Worker
andSharedWorker
classes:actual code fetched by worker URLs will then be handled by
window.fetch
(seemockFetch()
);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:
-
Both
title
andcookie
can be set and read without changing the actual properties of the current document. -
The
history
API is mocked and bound to themockLocation
object to return the same values and provide consistency. -
Hoot returns a
mockLocation
object to use instead ofwindow.location
, but this relies on the use of an indirection in the actual production code.Importante
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 themockLocation
object. -
Most used navigator features, such as the
clipboard
API anduserAgent
, have been mocked to hijack their actual behaviors. Itspermissions
object has been bound to a global mock of the permissions API. -
Notifications have been mocked, with the “notification” permissions bound to the global mocked permissions API.
-
Permissions can enable or disable other APIs by being given the
"granted"
or"denied"
statuses. This can be done through themockPermission
helper. -
localStorage
andsessionStorage
both point to “virtual” storages. -
Touch features can be force-activated or deactivated globally for a given test/suite using the
mockTouch()
helper. It will mock both the presence of touch handlers likeontouchstart
on window, as well as the"pointer"
media being set tofine
orcoarse
.
Related helpers¶
- mockPermission(name[, value])¶
Sets the given value for the given permission. This allows to enable or prevent certain APIs (see the Permissions API).
// Prevents the whole notification API from working mockPermission("notifications", "denied");
- mockTouch(setTouch)¶
Toggles touch features on or off in the current
Window
.