HOOT

概述

HOOT 是一个用 Owl 编写的测试框架,其主要特性包括:

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

  • 显示直观的界面以查看和筛选测试结果;

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

  • 提供可模拟各种全局对象的底层辅助函数。

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

  • @odoo/hoot-dom:(可在 tours 中使用)辅助函数:

    • 与 DOM 进行 交互,例如 :js click() 和 :js press();

    • 从 DOM 中 查询 元素,例如 :js queryAll() 和 :js waitFor();

  • @odoo/hoot:(仅可用于单元测试)测试框架的全部特性:

    • test、describe 和 expect

    • 测试钩子,例如 after 和 afterEach

    • 使用 getFixture 进行夹具处理

    • 日期与时间处理,例如 mockDate 或 advanceTime

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

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

注解

本节文档并不打算列出 Hoot 中可用的 所有 辅助函数(完整列表可在 @odoo/hoot 模块本身中找到)。本节旨在展示最常用的辅助函数,并说明导致测试框架形成当前形态的一些决策。

运行测试

在 Odoo 中,访问 /web/tests 网址即可运行前端单元测试。调用测试运行器所需的大部分设置都已就绪:

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

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

进入测试页面后,测试将按顺序运行,结果会显示在控制台和 GUI 中(如果不是以 headless 模式运行)。

运行器选项

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

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

  • 或通过 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

    测试运行器运行所在的环境。该参数用于确定其他功能的默认值,即:

    • 用户代理;

    • 触摸支持;

    • 视口的预期尺寸。

  • showdetail

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

  • tag

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

  • timeout

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

注解

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

编写测试

测试

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

以下是一个简单的示例:

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

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

describe 分组

大多数情况下,测试并没有那么简单。它们通常需要一些初始化和清理,有时还需要组合到一个套件中。这正是 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 函数是该框架的主要断言函数。它用于断言某个值或对象是否符合预期,或是否处于应有的状态。为此,它提供了若干修饰符和种类丰富的匹配器。

修饰符

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。

常规匹配器

匹配器决定对被测值执行何种操作。有些会直接使用该值,而另一些会先 转换 该值再对其执行断言(即 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 匹配器

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

toBeChecked([options])

要求接收到的 Target 为 "checked",或者在同名选项设为 true 时处于 "indeterminate" 状态。

  • 参数

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

  • 示例

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

要求接收到的 Target 处于 “displayed” 状态,即:

  • 它具有边界框;

  • 它包含在根文档中。

  • 参数

    • 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” 状态,即:

  • 它具有边界框;

  • 它包含在根文档中;

  • 它没有被 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: 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"]);
    

静态方法

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

这些方法主要用于在当前测试的范围内注册步骤或错误,并在之后对其求值。

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

要求当前测试具有 expected 数量的断言。该数量不能小于 1。

注解

通常更推荐改用 :js expect.step() 和 :js expect.verifySteps(),因为它们更可靠,并且允许进行更广泛的测试。

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

要求当前测试具有 expected 数量的错误。

这也意味着,从此函数被调用之时起,测试在被视为失败之前会接受该数量的错误。

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

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

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

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

退货

boolean

要求接收到的匹配器与自测试开始或上次调用 :js expect.verifyErrors() 以来抛出的错误匹配。调用此匹配器将重置当前错误列表。

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

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

退货

boolean

要求接收到的步骤等于自测试开始或上次调用 :js 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>

与 :js expect.verifyErrors() 相同,但如果错误尚未被捕获,不会立即失败,而是等待一定的超时时间(默认:2000ms),以便之后捕获错误。

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

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

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

退货

Promise<boolean>

与 :js expect.verifySteps() 相同,但如果步骤尚未注册,不会立即失败,而是等待一定的超时时间(默认:2000ms),以便之后注册步骤。

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

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

DOM:查询

自定义 DOM 选择器

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

  • :contains(text)

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

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

  • :displayed

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

  • :empty

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

  • :eq(n)

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

  • :first

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

  • :focusable

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

  • :hidden

    匹配 不 处于 “visible” 状态的节点(参见 isVisible)

  • :iframe

    匹配属于 <iframe> 元素的节点,如果其已就绪则返回它们的 body

  • :last

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

  • :selected

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

  • :shadow

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

  • :scrollable

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

  • :value(text)

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

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

  • :visible

    匹配处于 “visible” 状态的节点(参见 isVisible)

查询与节点属性辅助函数

Hoot 提供了一些辅助函数,可以简洁而优雅地查询节点及其部分属性。这主要通过 queryX 系列辅助函数来实现:

queryAll(target[, options])

返回与给定的 Target 匹配的节点列表。此函数既可作为 模板字符串标签 使用(仅支持不带选项的字符串选择器),也可以按常规方式调用。

目标可以是:

  • 一个 Node 或节点的可迭代对象,或 Window 对象;

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

  • 一个表示 自定义选择器 的字符串(将从 root 选项中查询)。

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

  • count: 要匹配的精确节点数量(如果节点数量不匹配则抛出错误);

  • displayed: 节点是否必须为 “displayed”(参见 isDisplayed);

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

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

  • visible: 节点是否必须为 “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 执行 :js queryAll(),并返回属性值列表。

退货

string[] 属性值列表

queryAllProperties(target, property[, options])

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

退货

unknown[] 属性值列表

queryAllTexts(target[, options])

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

退货

string[] 文本内容列表

queryAllValues(target[, options])

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

退货

string[] 值列表

queryAttribute(target, attribute[, options])

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

退货

string 属性值

queryFirst(target[, options])

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

退货

Node | null 第一个匹配的节点

queryOne(target[, options])

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

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

退货

Node 单个节点

queryText(target[, options])

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

退货

string 匹配节点的文本

queryValue(target[, options])

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

退货

string 匹配节点的值

上述所有辅助函数都是同步的,即它们会立即尝试查询节点。不过某些用例需要等待元素任意长的时间,该时间因 UI 获取与渲染的复杂性而无法预先确定。

针对这类情况,Hoot 提供了 2 种方法,用于在特定时间范围(默认为 200 毫秒)内等待元素出现 / 消失:

waitFor(target[, options])

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

退货

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

waitForNone(target[, options])

与 :js waitFor() 相反,等待给定目标从 DOM 中消失。

退货

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

DOM:交互辅助函数

除了查询元素之外,通常还需要与元素进行交互。因此,Hoot 提供了用于模拟各种用户在元素上交互的辅助函数。

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

指针交互辅助函数:

指针交互辅助函数(例如 :js click() 或 :js drag())会在给定目标上,以及指针 理应 位于的任何先前元素上,模拟真实的指针移动与事件。

check(target[, options])

确保给定 Target 处于勾选状态。

如果它未被勾选,则会在输入控件上模拟一次 :js click()。如果点击后输入控件仍未勾选,则抛出错误。

退货

Promise<Event[]>

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

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

事件序列如下:

  • pointerdown

  • [桌面] mousedown

  • [触摸] touchstart

  • [目标不是活动元素] blur

  • [目标可获得焦点] focus

  • pointerup

  • [桌面] mouseup

  • [触摸] touchend

  • click

  • dblclick 如果点击未被阻止且当前点击计数为偶数

退货

Promise<Event[]>

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

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

退货

Promise<Event[]>

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

在给定 Target 上启动拖拽序列。

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

  • moveTo:将指针移动到给定目标;

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

  • 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

  • [桌面] mouseover

  • pointerenter

  • [桌面] mouseenter

  • pointermove

  • [桌面] mousemove

  • [触摸] touchmove

退货

Promise<Event[]>

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

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

事件序列如下:

  • pointerdown

  • [桌面] mousedown

  • [触摸] touchstart

  • [目标不是活动元素] blur

  • [目标可获得焦点] focus

退货

Promise<Event[]>

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

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

事件序列如下:

  • pointerup

  • [桌面] mouseup

  • [触摸] touchend

退货

Promise<Event[]>

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

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

事件序列如下:

  • [桌面] 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 处于未勾选状态。

如果它已被勾选,则会在输入控件上触发一次 :js click()。如果点击后输入控件仍处于勾选状态,则抛出错误。

退货

Promise<Event[]>

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

其他交互辅助函数:

其他交互辅助函数没有 target 参数。这是没有必要的,因为(例如)在键盘上按键是针对当前 活动元素 执行的。

clear([options])

清除当前 活动元素 的值。

这通过以下序列完成:

  • 按下 "Control" 和 "A" 以选中整个值;

  • 按下 "Backspace" 以删除该值;

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

退货

Promise<Event[]>

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

:js clear() 与 :js fill() 的组合:

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

  • 然后用给定值填充输入控件

退货

Promise<Event[]>

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

用给定的 value 填充当前 活动元素。此辅助函数适用于 <input> 和 <textarea> 元素,但 "checkbox" 和 "radio" 类型除外,这些类型应使用 check 辅助函数来选择。

如果目标是可编辑的输入控件,其字符串 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])

在当前 活动元素 上执行按键按下序列。

事件序列如下:

  • keydown

根据按下的键,还会执行其他操作:

  • Tab:聚焦下一个(使用 shift 时为上一个)可获得焦点的元素;

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

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

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

  • Space:如果目标是 <input type="checkbox"> 元素,则在其上触发 click 事件。

退货

Promise<Event[]>

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

在当前 活动元素 上执行按键抬起序列。

事件序列如下:

  • keyup

退货

Promise<Event[]>

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

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

事件序列如下:

  • pointermove

  • [桌面] mousemove

  • [触摸] touchmove

  • pointerout

  • [桌面] mouseout

  • pointerleave

  • [桌面] mouseleave

退货

Promise<Event[]>

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

在当前 活动元素 上执行键盘事件序列。

事件序列如下:

  • 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

目标将被调整为给定尺寸,并由 !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[]>

模拟

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

在测试中(大多数情况下)还经常需要强制操作这些特性或改变它们的行为,因此提供了与这些模拟特性交互的辅助函数。以下各节将列出主要的模拟特性以及与之交互的方式。

时间

大多数异步特性都会被模拟:”timers”(setTimeout、setInterval 和 requestAnimationFrame)、Date 和 performance 都表现正常,但可以手动取消或加速,以大幅缩短测试的实际耗时。例如:所有 “timers” 都会在每个测试结束时取消,以避免对下一个测试产生副作用。

重要

有两种主要的时序行为 不会 被模拟:

  • Promise 对象及相关 API;

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

网络

一般而言,我们不希望在测试中执行真实的网络调用。为此,所有对 fetch 和 XMLHttpRequest 的调用都被重新路由到传给 :js mockFetch() 的函数。

注解

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

相关辅助函数

mockFetch([fetchFn])

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

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 处理(参见 :js mockFetch());

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

mockWorker([onWorkerConnected])

激活模拟的 Worker 和 SharedWorker 类:

  • 由 worker URL 获取的实际代码随后将由 window.fetch 处理(参见 :js mockFetch());

  • onWorkerConnected 回调将在 worker 创建后被调用。

值得注意的全局特性

以下特性可能并未添加专门的模拟实现,但它们无需改变其原本要改变的实际属性即可按预期工作:

  • 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

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

  • Permissions

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

  • Storage

    localStorage 和 sessionStorage 都指向“虚拟”存储。

  • Touch

    使用 :js mockTouch() 辅助函数,可以为给定的测试/套件全局强制启用或停用触摸特性。它会同时模拟 window 上诸如 ontouchstart 之类的触摸事件处理器的存在,以及 "pointer" 媒体被设置为 fine 或 coarse。

相关辅助函数

mockPermission(name[, value])

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

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

在当前 Window 中开启或关闭触摸特性。