From 1a576f224bea964844939f8df1b09d1ad4c51403 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Fri, 1 Mar 2024 16:51:25 +0000 Subject: [PATCH 1/2] [IMP] web: HOOT - documentation X-original-commit: eb756332c9b7a6b8effb56e091d5766f0b01769b --- content/developer/reference/frontend.rst | 1 + .../reference/frontend/unit_testing.rst | 90 + .../reference/frontend/unit_testing/hoot.rst | 1662 +++++++++++++++++ .../frontend/unit_testing/mock_server.rst | 383 ++++ .../frontend/unit_testing/web_helpers.rst | 187 ++ 5 files changed, 2323 insertions(+) create mode 100644 content/developer/reference/frontend/unit_testing.rst create mode 100644 content/developer/reference/frontend/unit_testing/hoot.rst create mode 100644 content/developer/reference/frontend/unit_testing/mock_server.rst create mode 100644 content/developer/reference/frontend/unit_testing/web_helpers.rst 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/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..beb53450c0 --- /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 production code) 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 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 `