HOOT¶
概述¶
HOOT 是一个使用 Owl 编写的测试框架,其关键特性如下:
注册并运行测试与测试套件;
提供一个直观的界面以查看和筛选测试结果;
提供与 DOM 交互以模拟用户操作的方式;
提供底层辅助工具以模拟各种全局对象。
因此,它作为 lib/ 集成在 Odoo 代码库中,并导出 2 个主要模块:
@odoo/hoot-dom:(可用于 tours)辅助工具用于:从 DOM 查询元素 ,如
queryAll()和waitFor();
@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估算 的每秒渲染帧数,用于模拟动画帧。(默认值:
60fps)
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:anyoptions:{ message?: string }
示例
expect("foo").toBe("foo"); expect({ foo: 1 }).not.toBe({ foo: 1 });
- toBeCloseTo(expected[, options])¶
期望接收到的值与
expected值 近似,精确到给定的位数(默认为 2)。参数
expected:anyoptions:{ 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:numberoptions:{ message?: string }
示例
expect(5).toBeGreaterThan(-1); expect(4 + 2).toBeGreaterThan(5);
- toBeInstanceOf(cls[, options])¶
期望接收到的值是给定
cls的实例。参数
cls:Functionoptions:{ message?: string }
示例
expect({ foo: 1 }).not.toBeInstanceOf(Object); expect(document.createElement("div")).toBeInstanceOf(HTMLElement);
- toBeLessThan(max[, options])¶
期望接收到的值 严格小于
max。参数
max:numberoptions:{ message?: string }
示例
expect(5).toBeLessThan(10); expect(8 - 6).toBeLessThan(3);
- toBeOfType(type[, options])¶
期望接收到的值属于给定的
type。参数
type:stringoptions:{ message?: string }
示例
expect("foo").toBeOfType("string"); expect({ foo: 1 }).toBeOfType("object");
- toBeWithin(min, max[, options])¶
期望接收到的值 介于
min和max之间(包含两端)。参数
min:numbermax:numberoptions:{ message?: string }
示例
expect(3).toBeWithin(3, 9); expect(-8.5).toBeWithin(-20, 0); expect(100).toBeWithin(50, 100);
- toEqual(expected[, options])¶
期望接收到的值与
expected值 深度相等。参数
expected:anyoptions:{ message?: string }
示例
expect(["foo"]).toEqual(["foo"]); expect({ foo: 1 }).toEqual({ foo: 1 });
- toHaveLength(length[, options])¶
期望接收到的值具有给定的
length长度。接收到的值可以是任何Iterable或Object。参数
length:numberoptions:{ 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:anyoptions:{ 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 | RegExpoptions:{ message?: string }
示例
expect(new Error("foo")).toMatch("foo"); expect("a foo value").toMatch(/fo.*ue/);
- toThrow(matcher[, options])¶
期望接收到的
Function在被调用后抛出错误。参数
matcher:string | number | RegExpoptions:{ 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:stringvalue:string | number | RegExpoptions:{ 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:numberoptions:{ 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 | RegExpoptions:{ 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 | RegExpoptions:{ 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:stringvalue:anyoptions:{ 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> | Targetoptions:{ 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 | RegExpoptions:{ message?: string, raw?: boolean }
示例
expect("p").toHaveText("lorem ipsum dolor sit amet"); expect("header h1").toHaveText(/odoo/i);
- toHaveValue(value[, options])¶
期望接收到的
Target的值满足以下之一:与给定的字符串或数字严格相等;
与给定的正则表达式匹配;
包含与给定
files列表匹配的文件对象。
参数
value:anyoptions:{ 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。返回值是单个节点,而不是节点列表。
- 返回
Nodea 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>,包含第一个匹配的节点
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 可聚焦]
focuspointerup[desktop]
mouseup[touch]
touchendclickdblclick,条件为点击未被阻止且当前点击次数为偶数
- 返回
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]
mouseoverpointerenter[desktop]
mouseenterpointermove[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]
wheelscroll
- 返回
Promise<Event[]>
scroll("body", { y: 0 }); // Scrolls to the top of <body>
- setInputRange(target, value[, options])¶
将给定值设置到当前的 “input[type=range]”
Target上。事件序列如下:
pointerdowninputchangepointerup
- 返回
Promise<Event[]>
其他交互辅助方法:¶
其他交互辅助方法没有 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])¶
-
首先,清除输入值(如果有的话)
然后将输入填充为给定的值
- 返回
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]
touchmovepointerout[desktop]
mouseoutpointerleave[desktop]
mouseleave
- 返回
Promise<Event[]>
leave("button"); // Moves out of <button>
- press(keyStrokes[, options])¶
在当前 active element 上执行一次键盘事件序列。
事件序列如下:
keydownkeyup
- 返回
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回调将被调用。
值得注意的全局功能¶
以下功能可能没有专门的模拟版本,但它们按预期工作,无需改变其应改变的实际属性:
-
title和cookie均可设置和读取,而不会改变当前文档的实际属性。 -
historyAPI 被模拟并绑定到mockLocation对象,以返回相同的值并提供一致性。 -
Hoot 返回一个
mockLocation对象以代替window.location,但这依赖于在实际生产代码中使用间接引用。重要
此功能只有在生产代码与
window.location调用之间存在间接引用时才有效。在 Odoo 中它之所以可行,是因为@web/core/browser模块提供了这样的间接引用,并且该模块在测试环境中被模拟以重定向到mockLocation对象。 -
最常用的 navigator 功能,例如
clipboardAPI 和userAgent,已被模拟以劫持其实际行为。其permissions对象被绑定到全局的权限 API 模拟。 -
通知(Notifications)已被模拟,其中 “notification” 权限绑定到全局的权限 API 模拟。
-
通过被赋予
"granted"或"denied"状态,权限 API 可以启用或禁用其他 API。这可以通过mockPermission辅助函数完成。 -
localStorage和sessionStorage均指向”虚拟”存储。 -
可以使用
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中开启或关闭触摸功能。