HOOT

概述

HOOT 是一个使用 Owl 编写的测试框架,其关键特性如下:

  • 注册并运行测试与测试套件;

  • 提供一个直观的界面以查看和筛选测试结果;

  • 提供与 DOM 交互以模拟用户操作的方式;

  • 提供底层辅助工具以模拟各种全局对象。

因此,它作为 lib/ 集成在 Odoo 代码库中,并导出 2 个主要模块:

  • @odoo/hoot-dom:(可用于 tours)辅助工具用于:

  • @odoo/hoot:(仅用于单元测试)包含测试框架的所有特性:

    • test、describe 和 expect

    • 测试钩子如 after 和 afterEach

    • 通过 getFixture 进行 fixture 处理

    • 类似 mockDate 或 advanceTime 的日期和时间处理

    • 通过 mockFetch() 或 mockWebSocket() 模拟网络响应

    • @odoo/hoot-dom 导出的所有辅助函数

注解

本节文档并不旨在列出 Hoot 中可用的 所有 辅助函数(完整列表可在 @odoo/hoot 模块本身中找到)。此处的目标是展示最常用的辅助函数,并解释促成测试框架当前形态的一些决策。

运行测试

在 Odoo 中,前端单元测试可以通过访问 /web/tests URL 来运行。调用测试运行器所需的大部分配置已就位:

  • web.assets_unit_tests 资产包已定义,并会收集大多数插件中定义的所有测试;

  • 一个 start.hoot.js 文件负责使用其导出的 start 入口函数调用测试运行器。

访问测试页面时,测试将依次运行,结果会显示在控制台和 GUI 中(如果未以 headless 模式运行)。

Runner options

测试运行器可以通过以下方式配置:

  • 通过界面(使用配置下拉菜单和搜索栏);

  • 或者通过 URL 查询参数(例如用 ?headless 以无头模式运行)。

以下是测试运行器可用的选项列表:

  • bail

    达到该数量的失败测试后,测试运行器将停止。假值(包括 0)表示运行器永不被中止。(默认值:0)

  • debugTest

    与 FILTER_SCHEMA.test 过滤器相同,同时还会将测试运行器置于 “debug” 模式。更多信息参见 TestRunner.debug。(默认值:false)

  • fps

    设置每秒帧数的值(该值将被转换为毫秒,并用于 advanceFrame)

  • filter

    搜索字符串,将基于测试/套件的完整名称(包括其父套件)和标签来筛选匹配的测试/套件。(默认值:"")

  • frameRate

    估算 的每秒渲染帧数,用于模拟动画帧。(默认值:60 fps)

  • fun

    让气氛更轻松。(默认值:false)

  • headless

    是否渲染测试运行器的用户界面。(默认值:false)

  • id

    要单独运行的套件或测试的 ID。每个任务的 ID 都是基于其完整名称确定性生成的。

  • loglevel

    测试运行器使用的日志级别。级别越高,显示的日志越多:

    • 0:仅显示运行器日志(默认值)

    • 1:还会记录所有套件的结果

    • 2:还会记录所有测试的结果

    • 3:还会记录每个测试的调试信息

  • manual

    页面加载后是否必须手动启动测试运行器(默认自动启动)。(默认值:false)

  • notrycatch

    移除包裹在每个测试运行函数周围的 try .. catch 语句所提供的保护,让错误冒泡到浏览器。(默认值:false)

  • order

    决定测试的执行顺序:

    • "fifo":测试将按照文件系统中的声明顺序依次运行;

    • "lifo":测试将按照相反的顺序依次运行;

    • "random":在其父套件内部打乱测试和套件的顺序。

  • preset

    测试运行器所处的环境。此参数用于确定其他功能的默认值,具体包括:

    • 用户代理(user agent);

    • 触摸支持;

    • 视口的预期尺寸。

  • showdetail

    决定失败的测试在 UI 中如何展开。(默认值:"first-fail")

  • tag

    要单独运行的测试和套件的标签名称(不区分大小写)。(默认值:空)

  • timeout

    持续时间(以毫秒计),到达该时长后测试将自动失败。(默认值:5 秒)

注解

选择要运行的测试和套件时,会在 包含 过滤器之间隐式应用 OR。这意味着添加更多的包含过滤器会导致运行更多的测试。这适用于 filter、id 和 tag 过滤器(而 排除 过滤器则会从待运行测试列表中移除匹配的测试)。

Writing tests

Test

编写测试可以非常简单,只需调用 test 函数并传入一个名称和一个包含测试逻辑的函数即可。

下面是一个简单的示例:

import { expect, test } from "@odoo/hoot";

test("My first test", () => {
    expect(2 + 2).toBe(4);
});

Describe

大多数时候,测试并不那么简单。它们通常需要一些前置设置(setup)和清理(teardown),有时还需要被分组到一个套件中。这正是 describe 函数发挥作用的地方。

以下是如何声明一个套件,并在其中声明一个测试:

import { describe, expect, test } from "@odoo/hoot";

describe("My first suite", () => {
    test("My first test", () => {
        expect(2 + 2).toBe(4);
    });
});

重要

在 Odoo 中,所有测试文件都在隔离环境中运行,并被包裹在一个全局 describe 块中(套件的名称为测试文件的 路径)。

基于此,你不需要在测试文件中声明套件;但如果你出于组织或打标签的目的仍想拆分该文件的套件,可以在同一文件中声明子套件。

Expect

expect 函数是该框架的主要断言函数。它用于断言一个值或对象是其期望的样子,或处于其应有的状态。为此,它提供了一些修饰符和大量的匹配器。

Modifiers

expect 修饰符是一个 getter,返回另一组 被修改的 匹配器,这些匹配器将以特定的方式工作。

  • not

    反转后续匹配器的结果:当匹配器失败时,它将成功。

    expect(true).not.toBe(false);
    
  • resolves

    等待值(Promise)被 “resolved” 后,再以已解决的值为参数运行后续匹配器。

    await expect(Promise.resolve(42)).resolves.toBe(42);
    
  • rejects

    等待值(Promise)被 “rejected” 后,再以拒绝原因为参数运行后续匹配器。

    await expect(Promise.reject("error")).rejects.toBe("error");
    

注解

resolves 和 rejects 修饰符仅在值是 Promise 时可用,并且会返回一个 Promise,该 Promise 将在断言完成时被解决。

Regular matchers

匹配器决定了如何处理被测试的值。有些匹配器直接采用该值的原样,有些则在对其执行断言之前 转换 该值(即 DOM 匹配器)。

请注意,所有匹配器的最后一个参数是一个包含附加选项的可选字典,可以在其中提供一个自定义断言 message,以增加上下文/细节。

第一组匹配器基于原始类型或对象,也是最常见的:

toBe(expected[, options])

期望接收到的值与 expected 值 严格相等。

  • 参数

    • expected: any

    • options: { message?: string }

  • 示例

    expect("foo").toBe("foo");
    expect({ foo: 1 }).not.toBe({ foo: 1 });
    
toBeCloseTo(expected[, options])

期望接收到的值与 expected 值 近似,精确到给定的位数(默认为 2)。

  • 参数

    • expected: any

    • options: { message?: string, digits?: number }

  • 示例

    expect(0.2 + 0.1).toBeCloseTo(0.3);
    expect(3.51).toBeCloseTo(3.5, { digits: 1 });
    
toBeEmpty([options])

期望接收到的值为空:

  • iterable:无条目

  • object:无键

  • node:无内容(即无值或文本)

  • 其他情况:假值(false、0、""、null、undefined)

  • 参数

    • options: { message?: string }

  • 示例

    expect({}).toBeEmpty();
    expect(["a", "b"]).not.toBeEmpty();
    expect(queryOne("input")).toBeEmpty();
    
toBeGreaterThan(min[, options])

期望接收到的值 严格大于 min。

  • 参数

    • min: number

    • options: { message?: string }

  • 示例

    expect(5).toBeGreaterThan(-1);
    expect(4 + 2).toBeGreaterThan(5);
    
toBeInstanceOf(cls[, options])

期望接收到的值是给定 cls 的实例。

  • 参数

    • cls: Function

    • options: { message?: string }

  • 示例

    expect({ foo: 1 }).not.toBeInstanceOf(Object);
    expect(document.createElement("div")).toBeInstanceOf(HTMLElement);
    
toBeLessThan(max[, options])

期望接收到的值 严格小于 max。

  • 参数

    • max: number

    • options: { message?: string }

  • 示例

    expect(5).toBeLessThan(10);
    expect(8 - 6).toBeLessThan(3);
    
toBeOfType(type[, options])

期望接收到的值属于给定的 type。

  • 参数

    • type: string

    • options: { message?: string }

  • 示例

    expect("foo").toBeOfType("string");
    expect({ foo: 1 }).toBeOfType("object");
    
toBeWithin(min, max[, options])

期望接收到的值 介于 min 和 max 之间(包含两端)。

  • 参数

    • min: number

    • max: number

    • options: { message?: string }

  • 示例

    expect(3).toBeWithin(3, 9);
    expect(-8.5).toBeWithin(-20, 0);
    expect(100).toBeWithin(50, 100);
    
toEqual(expected[, options])

期望接收到的值与 expected 值 深度相等。

  • 参数

    • expected: any

    • options: { message?: string }

  • 示例

    expect(["foo"]).toEqual(["foo"]);
    expect({ foo: 1 }).toEqual({ foo: 1 });
    
toHaveLength(length[, options])

期望接收到的值具有给定的 length 长度。接收到的值可以是任何 Iterable 或 Object。

  • 参数

    • length: number

    • options: { message?: string }

  • 示例

    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])

期望接收到的值包含一个符合给定形状的 item。

接收到的值可以是可迭代对象或对象(如果是对象,则 item 应是表示该对象中某个条目的键或元组)。

请注意,这不是严格比较:该元素将与可迭代对象中的每个元素进行深度相等匹配。

  • 参数

    • item: any

    • options: { message?: string }

  • 示例

    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])

期望接收到的值与给定的 matcher 匹配。

  • 参数

    • matcher: string | number | RegExp

    • options: { message?: string }

  • 示例

    expect(new Error("foo")).toMatch("foo");
    expect("a foo value").toMatch(/fo.*ue/);
    
toThrow(matcher[, options])

期望接收到的 Function 在被调用后抛出错误。

  • 参数

    • matcher: string | number | RegExp

    • options: { message?: string }

  • 示例

    expect(() => { throw new Error("Woops!") }).toThrow(/woops/i);
    await expect(Promise.reject("foo")).rejects.toThrow("foo");
    

DOM matchers

接下来这一组匹配器基于节点,用于断言一个节点或一组节点的状态。它们通常将 自定义选择器 作为 expect 函数的参数(尽管也接受 Node 或 Node 的可迭代对象)。

toBeChecked([options])

期望接收到的 Target 为 "checked" 状态,或者当同名选项被设置为 true 时为 "indeterminate" 状态。

  • 参数

    • options: { message?: string, indeterminate?: boolean }

  • 示例

    expect("input[type=checkbox]").toBeChecked();
    
toBeDisplayed([options])

期望接收到的 Target 处于 “displayed” 状态,即:

  • 它具有边界框(bounding box);

  • 它包含在根文档中。

  • 参数

    • options: { message?: string }

  • 示例

    expect(document.body).toBeDisplayed();
    expect(document.createElement("div")).not.toBeDisplayed();
    
toBeEnabled([options])

期望接收到的 Target 处于 “enabled” 状态,即它匹配 :enabled 伪类选择器。

  • 参数

    • options: { message?: string }

  • 示例

    expect("button").toBeEnabled();
    expect("input[type=radio]").not.toBeEnabled();
    
toBeFocused([options])

期望接收到的 Target 在其所属文档中处于 “focused” 状态。

  • 参数

    • options: { message?: string }

toBeVisible([options])

期望接收到的 Target 处于 “visible” 状态,即:

  • 它具有边界框(bounding box);

  • 它包含在根文档中;

  • 它未被 CSS 属性隐藏。

  • 参数

    • options: { message?: string }

  • 示例

    expect(document.body).toBeVisible();
    expect("[style='opacity: 0']").not.toBeVisible();
    
toHaveAttribute(attribute, value[, options])

期望接收到的 Target 已设置给定的属性,并且该属性值(若提供了值)与给定的 value 匹配。

  • 参数

    • attribute: string

    • value: string | number | RegExp

    • options: { message?: string }

  • 示例

    expect("a").toHaveAttribute("href");
    expect("script").toHaveAttribute("src", "./index.js");
    
toHaveClass(className[, options])

期望接收到的 Target 具有给定的类名。

  • 参数

    • className: string | string[]

    • options: { message?: string }

  • 示例

    expect("button").toHaveClass("btn btn-primary");
    expect("body").toHaveClass(["o_webclient", "o_dark"]);
    
toHaveCount(amount[, options])

期望接收到的 Target 恰好包含 amount 个元素。注意 amount 参数可以省略,在这种情况下该函数将期望 至少 有一个元素。

  • 参数

    • amount: number

    • options: { message?: string }

  • 示例

    expect(".o_webclient").toHaveCount(1);
    expect(".o_form_view .o_field_widget").toHaveCount();
    expect("ul > li").toHaveCount(4);
    
toHaveInnerHTML(expected[, options])

期望接收到的 Target 的 innerHTML 与 expected 值匹配(经过格式化后)。

  • 参数

    • expected: string | RegExp

    • options: { message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }

  • 示例

    expect(".my_element").toHaveInnerHTML(`
        Some <strong>text</strong>
    `);
    
toHaveOuterHTML(expected[, options])

期望接收到的 Target 的 outerHTML 与 expected 值匹配(经过格式化后)。

  • 参数

    • expected: string | RegExp

    • options: { message?: string, type?: "html" | "xml", tabSize?: number, keepInlineTextNodes?: boolean }

  • 示例

    expect(".my_element").toHaveOuterHTML(`
        <div class="my_element">
            Some <strong>text</strong>
        </div>
    `);
    
toHaveProperty(property, value[, options])

期望接收到的 Target 的给定属性值与给定的 value 匹配。如果未提供值:匹配器将改为检查该属性是否存在于目标上。

  • 参数

    • property: string

    • value: any

    • options: { message?: string }

  • 示例

    expect("button").toHaveProperty("tabIndex", 0);
    expect("input").toHaveProperty("ontouchstart");
    expect("script").toHaveProperty("src", "./index.js");
    
toHaveRect(rect[, options])

期望接收到的 Target 的 DOMRect 与给定的 rect 对象匹配。rect 对象可以是以下之一:

  • 一个 DOMRect 对象;

  • 一个 CSS 选择器字符串(用于获取 唯一 匹配元素的 rect);

  • 一个节点。

如果得到的 rect 值是节点,则两个节点的 rect 将被比较。

  • 参数

    • rect: Partial<DOMRect> | Target

    • options: { message?: string, trimPadding?: boolean }

  • 示例

    expect("button").toHaveRect({ x: 20, width: 100, height: 50 });
    expect("button").toHaveRect(".container");
    
toHaveStyle(style[, options])

期望接收到的 Target 匹配给定的样式属性。

  • 参数

    • style: string | Record<string, string | RegExp>

    • options: { message?: string }

  • 示例

    expect("button").toHaveStyle({ color: "red" });
    expect("p").toHaveStyle("text-align: center");
    
toHaveText(text[, options])

期望接收到的 Target 的 text 内容满足以下之一:

  • 与给定的字符串严格相等;

  • 与给定的正则表达式匹配。

注意:使用 innerHTML 来获取文本内容,以考虑 CSS 可见性。这也意味着来自子元素的文本值会以换行符作为分隔符进行拼接。

  • 参数

    • text: string | RegExp

    • options: { message?: string, raw?: boolean }

  • 示例

    expect("p").toHaveText("lorem ipsum dolor sit amet");
    expect("header h1").toHaveText(/odoo/i);
    
toHaveValue(value[, options])

期望接收到的 Target 的值满足以下之一:

  • 与给定的字符串或数字严格相等;

  • 与给定的正则表达式匹配;

  • 包含与给定 files 列表匹配的文件对象。

  • 参数

    • value: any

    • options: { message?: string }

  • 示例

    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

expect 辅助函数还包含一些静态方法,可用于执行一个独立的测试流程,该流程不与某一特定时刻的某个特定值绑定。

这些方法主要用于在当前测试的作用域内注册步骤或错误,并在之后对它们进行评估。

expect.assertions(expected)
参数
  • expected (number()) –

期望当前测试具有 expected 数量的断言。该数字不能小于 1。

注解

一般更推荐使用 expect.step() 和 expect.verifySteps(),因为它们更可靠,并且能够进行更充分的测试。

expect.errors(expected)
参数
  • expected (number()) –

期望当前测试具有 expected 数量的错误。

这也意味着,从调用此函数的时刻起,测试将接受该数量的错误,然后才会被视为失败。

expect.step(value)
参数
  • value (unknown()) –

为当前测试注册一个步骤,该步骤可由 expect.verifySteps() 消耗。未消耗的步骤将导致测试失败。

expect.verifyErrors(errors[, options])
参数
  • errors (unknown[]()) –

  • options ({ message?: string }()) –

返回

布尔值

期望所接收的匹配器能够匹配自测试开始或上次调用 expect.verifyErrors() 以来所抛出的错误。调用此匹配器将会重置当前错误列表。

expect.verifyErrors([/RPCError/, /Invalid domain AST/]);
expect.verifySteps(steps[, options])
参数
  • steps (unknown[]()) –

  • options ({ ignoreOrder?: boolean, message?: string, partial?: boolean }()) –

返回

布尔值

期望所接收的步骤与自测试开始或上次调用 expect.verifySteps() 以来所产生的步骤相等。调用此匹配器将会重置当前步骤列表。

expect.step("web_read_group");
expect.step([1, 2]);
expect.verifySteps(["web_read_group", [1, 2]]);
expect.waitForErrors(errors[, options])
参数
  • errors (unknown[]()) –

  • options ({ message?: string }()) –

返回

Promise<boolean>

与 expect.verifyErrors() 类似,但若错误尚未捕获,不会立即失败,而是会等待一段超时时间(默认:2000 毫秒),以便稍后捕获这些错误。

检查将在开始时、超时结束时以及每次检测到错误时执行。

fetch("invalid/url");
await expect.waitForErrors([/RPCError/]);
expect.waitForSteps(steps[, options])
参数
  • steps (unknown[]()) –

  • options ({ ignoreOrder?: boolean, message?: string, partial?: boolean }()) –

返回

Promise<boolean>

与 expect.verifySteps() 类似,但若步骤尚未注册,不会立即失败,而是会等待一段超时时间(默认:2000 毫秒),以便稍后注册步骤。

检查将在开始时、超时结束时以及每次注册步骤时执行。

// ... step on each 'web_read_group' call
fetch(".../call_kw/web_read_group");
await expect.waitForSteps(["web_read_group"]);

DOM:查询

Custom DOM selectors

本节简要介绍 Hoot 中的 DOM 选择器,它们支持额外的伪类,可用于根据非标准特性来定位元素,例如其文本内容或其在文档中的全局位置。

  • :contains(text)

    匹配文本内容与给定 text 相符的节点

    • 给定的 text 支持正则表达式语法(例如 :contains(/^foo.+/)),且不区分大小写(除非在正则末尾使用 i 标志)

  • :displayed

    匹配处于 “已显示” 状态的节点(参见 isDisplayed)

  • :empty

    匹配内容为空(值或文本内容)的节点

  • :eq(n)

    根据其全局位置(从 0 开始索引)返回第 n 个节点;

  • :first

    返回在整个文档中匹配该选择器的第一个节点

  • :focusable

    匹配可被 “聚焦” 的节点(参见 isFocusable)

  • :hidden

    匹配 不“可见”的节点(参见 isVisible)

  • :iframe

    匹配 <iframe> 元素节点,若其 body 已就绪则返回其 body

  • :last

    返回在整个文档中匹配该选择器的最后一个节点

  • :selected

    匹配已被选中的节点(例如 <option> 元素)

  • :shadow

    匹配具有 shadow root 的节点,并返回其 shadow root

  • :scrollable

    匹配可滚动的节点(参见 isScrollable)

  • :value(text)

    匹配值与给定 text 相符的节点

    • 给定的 text 支持正则表达式语法(例如 :value(/^foo.+/)),且不区分大小写(除非在正则末尾使用 i 标志)

  • :visible

    匹配 “可见” 的节点(参见 isVisible)

查询与节点属性辅助方法

Hoot 提供了简洁优雅的辅助方法来查询节点及其部分属性。这主要通过使用 queryX 辅助方法来实现:

queryAll(target[, options])

返回一个匹配给定 Target 的节点列表。此函数既可以作为 template literal tag 使用(仅支持不带 options 的字符串选择器),也可以按常规方式调用。

target 可以是

  • Node 对象(或可迭代的节点集合),或 Window 对象;

  • Document 对象(将被转换为其 body);

  • 表示一个 custom selector 的字符串(将从 root 选项处查询)。

可以指定一个 options 对象来过滤 1 结果:

  • count:要匹配的节点确切数量(若节点数量不符则抛出错误);

  • displayed:节点是否必须处于“已显示”状态(参见 isDisplayed);

  • focusable:节点是否必须可“聚焦”(参见 isFocusable);

  • root:在其中查询选择器的根节点(默认为当前 fixture);

  • visible:节点是否必须“可见”(参见 isVisible)。 * 此选项隐含 displayed

1

这些过滤器(count 和 root 除外)与在给定的选择器字符串的最后一个分组上使用同名的伪类效果相同,例如:

// These 2 will return the same result
queryAll`ul > li:visible`;
queryAll("ul > li", { visible: true });
返回

Node[]

queryAllAttributes(target, attribute[, options])

对给定的 target 执行 queryAll(),并返回属性值列表。

返回

string[] 属性值列表

queryAllProperties(target, property[, options])

对给定的 target 执行 queryAll(),并返回属性值列表。

返回

unknown[] 属性值列表

queryAllTexts(target[, options])

对给定的 target 执行 queryAll(),并返回文本内容列表。

返回

string[] 文本内容列表

queryAllValues(target[, options])

对给定的 target 执行 queryAll(),并返回值列表。

返回

string[] 值列表

queryAttribute(target, attribute[, options])

使用给定参数执行 queryOne(),并返回匹配节点上给定 attribute 的值。

返回

string 属性值

queryFirst(target[, options])

使用给定参数执行 queryAll(),并返回第一个结果或 null。

返回

Node | null 第一个匹配的节点

queryOne(target[, options])

使用给定参数执行 queryAll(),并强制使用 count: 1 选项以确保只有一个节点匹配给定的 Target。

返回值是单个节点,而不是节点列表。

返回

Node a single node

queryText(target[, options])

使用给定参数执行 queryOne(),并返回匹配节点的 text。

返回

string 匹配节点的文本

queryValue(target[, options])

使用给定参数执行 queryOne(),并返回匹配节点的 value。

返回

string 匹配节点的值

以上所有辅助方法都是同步的,这意味着它们会立即尝试查询节点。但某些用例要求元素在被查询前需等待一段任意的时间,而由于 UI 数据获取和渲染的复杂性,该时间通常无法提前得知。

针对此类场景,Hoot 提供了 2 种方法来等待元素在指定时间范围内(默认:200 毫秒)出现或消失:

waitFor(target[, options])

queryAll() 与 waitUntil() 的组合:等待给定 target 在 DOM 中匹配元素,并在其出现时返回第一个匹配节点(若已存在则立即返回)。

返回

Promise<Node>,包含第一个匹配的节点

waitForNone(target[, options])

与 waitFor() 相反,等待给定 target 从 DOM 中消失。

返回

Promise<number>,包含匹配节点的数量

DOM:交互辅助方法

除了查询元素外,通常还需要与其进行交互。为此,Hoot 提供了辅助方法来模拟在元素上进行的各类用户交互。

根据其参数,这些辅助方法可划分为两类: 基于指针的 交互辅助方法,以及 其他 辅助方法。

指针交互辅助方法:

指针交互辅助方法(如 click() 或 drag())会在给定的 target 上以及指针 本应 位于的任何前置元素上模拟实际的指针移动和事件。

check(target[, options])

确保给定的 Target 已被选中。

若未被选中,则会在该输入元素上模拟一次 click()。若点击后仍未选中,则抛出错误。

返回

Promise<Event[]>

check("input[type=checkbox]"); // Checks the first <input> checkbox element
click(target[, options])

在给定 Target 上执行一次点击序列。

事件序列如下:

  • pointerdown

  • [desktop] mousedown

  • [touch] touchstart

  • [target 不是当前活动元素] blur

  • [target 可聚焦] focus

  • pointerup

  • [desktop] mouseup

  • [touch] touchend

  • click

  • dblclick,条件为点击未被阻止且当前点击次数为偶数

返回

Promise<Event[]>

click("button"); // Clicks on the first <button> element
dblclick(target[, options])

在给定 Target 上执行两次 click() 序列。

返回

Promise<Event[]>

dblclick("button"); // Double-clicks on the first <button> element
drag(target[, options])

在给定 Target 上开始一次拖拽序列。

返回一组用于控制该序列的辅助函数:

  • moveTo:将指针移动到给定的 target;

  • drop:将被拖拽的元素放置到给定的 target 上(如果有的话);

  • cancel:取消拖拽序列。

返回

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])

在给定 Target 上执行一次悬停序列。

事件序列如下:

  • pointerover

  • [desktop] mouseover

  • pointerenter

  • [desktop] mouseenter

  • pointermove

  • [desktop] mousemove

  • [touch] touchmove

返回

Promise<Event[]>

hover("button"); // Hovers the first <button> element
pointerDown(target[, options])

在给定 Target 上执行指针按下操作。

事件序列如下:

  • pointerdown

  • [desktop] mousedown

  • [touch] touchstart

  • [target 不是当前活动元素] blur

  • [target 可聚焦] focus

返回

Promise<Event[]>

pointerDown("button"); // Focuses to the first <button> element
pointerUp(target[, options])

在给定 Target 上执行指针抬起操作。

事件序列如下:

  • pointerup

  • [desktop] mouseup

  • [touch] touchend

返回

Promise<Event[]>

pointerUp("body"); // Triggers a pointer up on the <body> element
scroll(target, position[, options])

在给定 Target 上执行一次滚动事件序列。

事件序列如下:

  • [desktop] wheel

  • scroll

返回

Promise<Event[]>

scroll("body", { y: 0 }); // Scrolls to the top of <body>
setInputRange(target, value[, options])

将给定值设置到当前的 “input[type=range]” Target 上。

事件序列如下:

  • pointerdown

  • input

  • change

  • pointerup

返回

Promise<Event[]>

uncheck(target[, options])

确保给定的 Target 未被选中。

若已选中,则会在该输入元素上触发一次 click()。若点击后仍为选中状态,则抛出错误。

返回

Promise<Event[]>

uncheck("input[type=checkbox]"); // Unchecks the first <input> checkbox element

其他交互辅助方法:

其他交互辅助方法没有 target 参数。这是不必要的,因为按键(例如)操作是在当前 active element 上进行的。

clear([options])

清除当前 active element 的值。

实现过程如下:

  • 按下 "Control" 与 "A" 以选中全部值;

  • 按下 "Backspace" 删除该值;

  • (可选)按下 "Enter" 触发 "change" 事件。

返回

Promise<Event[]>

clear(); // Clears the value of the current active element
edit(value[, options])

clear() 与 fill() 的组合:

  • 首先,清除输入值(如果有的话)

  • 然后将输入填充为给定的值

返回

Promise<Event[]>

fill("foo"); // Types "foo" in the active element
edit("Hello World"); // Replaces "foo" by "Hello World"
fill(value[, options])

将给定 value 填充到当前 active element。此辅助方法适用于 <input> 和 <textarea> 元素,但 "checkbox" 和 "radio" 类型除外,后者应使用 check 辅助方法来选中。

若 target 是可编辑的输入元素,其字符串 value 将逐字符输入,每个字符都会产生相应的键盘事件序列。此行为可通过传入 instantly 选项来覆盖,此时将改为模拟 control + v 键盘序列,从而将整个文本粘贴。

请注意,给定的值将追加到元素当前的值之后。

若活动元素为 <input type="file"/>,则 value 应为 File 或 File 对象列表。

返回

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])

在当前 active element 上执行一次按键按下序列。

事件序列如下:

  • keydown

此外,会根据所按下的键执行附加操作:

  • Tab:聚焦到下一个(按下 shift 时为上一个)可聚焦元素;

  • c:将当前选区复制到剪贴板;

  • v:将剪贴板当前内容粘贴到当前元素;

  • Enter:若 target 是 <button type="button"> 或 <form> 元素则提交表单,若 target 是 <input> 元素则在其上触发 change 事件;

  • Space:若 target 是 <input type="checkbox"> 元素则在其上触发 click 事件。

返回

Promise<Event[]>

keyDown(" "); // Space key
keyUp(keyStrokes[, options])

在当前 active element 上执行一次按键抬起序列。

事件序列如下:

  • keyup

返回

Promise<Event[]>

keyUp("Enter");
leave([options])

在当前 Window 上执行一次离开序列。

事件序列如下:

  • pointermove

  • [desktop] mousemove

  • [touch] touchmove

  • pointerout

  • [desktop] mouseout

  • pointerleave

  • [desktop] mouseleave

返回

Promise<Event[]>

leave("button"); // Moves out of <button>
press(keyStrokes[, options])

在当前 active element 上执行一次键盘事件序列。

事件序列如下:

  • keydown

  • keyup

返回

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]])

在当前 Window 上执行一次调整大小事件序列。

事件序列如下:

  • resize

target 将被调整为给定的尺寸,通过 !important 样式属性强制执行。

返回

Promise<Event[]>

resize("body", { width: 1000, height: 500 }); // Resizes <body> to 1000x500
select(value[, options])

对当前活动元素执行选择事件序列。此辅助函数仅适用于 <select> 元素。

事件序列如下:

  • change

返回

Promise<Event[]>

click("select[name=country]"); // Focuses <select> element
select("belgium"); // Selects the <option value="belgium"> element
setInputFiles(files[, options])

将给定的 File 列表交给当前的文件输入。此辅助函数仅在先前与文件输入交互过(通过点击它)后才起作用。

返回

Promise<Event[]>

unload([options])

在当前 Window 上触发一个 “beforeunload” 事件。

返回

Promise<Event[]>

Mocks

默认情况下,Hoot 会模拟许多底层功能:clipboard、fetch、localStorage 等。这些模拟旨在不产生任何干扰测试运行器或其他测试上下文的副作用,同时提供相同的接口,让测试可以无缝依赖这些功能。

出于测试需要(大部分情况下),常常需要强制对这些功能执行操作或改变其行为,因此提供了与这些模拟功能交互的辅助函数。以下各节将列出主要的模拟功能以及与其交互的方式。

Time

大多数异步功能被模拟了:”timers”(setTimeout、setInterval 和 requestAnimationFrame)、Date 和 performance 均按正常方式工作,但可以手动取消或加速以显著缩短测试的实际持续时间。例如:所有 “timers” 在每个测试结束时都会被取消,以避免给下一个测试造成副作用。

重要

有 2 种主要的计时行为是 不 被模拟的:

  • Promise 对象及相关 API;

  • OWL 的计时器函数:要等待 OWL 渲染函数完成,必须借助 animationFrame 辅助函数。

Network

通常,我们不想在测试中执行真实的网络调用。为确保这一点,所有对 fetch 和 XMLHttpRequest 的调用都会被重新路由到传给 mockFetch() 的函数。

注解

在 Odoo 中,这通常由 MockServer 隐式处理,它会由模拟环境创建,即每当使用 mountWithCleanup 辅助函数渲染组件时都会创建。

Related helpers

mockFetch([fetchFn])

通过将 fetch 函数替换为给定的 fetchFn 来模拟 fetch 函数。

fetchFn 的返回值被用作被模拟的 fetch 的响应,若其不符合所需格式,则被包装在一个 MockResponse 对象中。

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])

激活模拟的 WebSocket 类:

  • WebSocket 连接将由 window.fetch 处理(参见 mockFetch());

  • WebSocket 创建后,onWebSocketConnected 回调将被调用。

mockWorker([onWorkerConnected])

激活模拟的 Worker 和 SharedWorker 类:

  • 由 Worker URL 获取的实际代码将由 window.fetch 处理(参见 mockFetch());

  • Worker 创建后,onWorkerConnected 回调将被调用。

值得注意的全局功能

以下功能可能没有专门的模拟版本,但它们按预期工作,无需改变其应改变的实际属性:

  • Document

    title 和 cookie 均可设置和读取,而不会改变当前文档的实际属性。

  • History

    history API 被模拟并绑定到 mockLocation 对象,以返回相同的值并提供一致性。

  • Location

    Hoot 返回一个 mockLocation 对象以代替 window.location,但这依赖于在实际生产代码中使用间接引用。

    重要

    此功能只有在生产代码与 window.location 调用之间存在间接引用时才有效。在 Odoo 中它之所以可行,是因为 @web/core/browser 模块提供了这样的间接引用,并且该模块在测试环境中被模拟以重定向到 mockLocation 对象。

  • Navigator

    最常用的 navigator 功能,例如 clipboard API 和 userAgent,已被模拟以劫持其实际行为。其 permissions 对象被绑定到全局的权限 API 模拟。

  • Notification

    通知(Notifications)已被模拟,其中 “notification” 权限绑定到全局的权限 API 模拟。

  • Permissions

    通过被赋予 "granted" 或 "denied" 状态,权限 API 可以启用或禁用其他 API。这可以通过 mockPermission 辅助函数完成。

  • Storage

    localStorage 和 sessionStorage 均指向”虚拟”存储。

  • Touch

    可以使用 mockTouch() 辅助函数针对给定的测试/测试套件全局强制激活或停用触摸功能。它将同时模拟 window 上像 ontouchstart 这样的触摸处理器的存在,以及 "pointer" 媒体设置为 fine 或 coarse。

Related helpers

mockPermission(name[, value])

为给定的权限设置给定的值。这可以启用或阻止某些 API(参见 Permissions API)。

// Prevents the whole notification API from working
mockPermission("notifications", "denied");
mockTouch(setTouch)

在当前 Window 中开启或关闭触摸功能。