Mochitest 常见问题解答

SSL 和 https 启用测试

Mochitests 必须从 http://mochi.test/ 运行才能成功。但是,某些测试可能需要使用其他协议、主机或端口来测试跨源功能。

Mochitest 测试框架通过代理自动配置和 SSL 隧道将原始服务器的所有内容镜像到各种其他服务器来满足此需求。测试提供服务的方案、主机和端口的完整列表在 build/pgo/server-locations.txt 中指定。

那里描述的来源并不相同,因为其中一些为测试目的指定了特定的 SSL 证书,而另一些则允许该服务器上的页面请求提升的权限;阅读文件以获取完整详细信息。

其工作原理如下:Mochitest 测试框架包含首选项值,这些值会导致浏览器使用代理自动配置将请求的 URL 与服务器匹配。 network.proxy.autoconfig_url 首选项设置为一个 data: URL,该 URL 编码了 JavaScript 函数 FindProxyForURL,该函数确定给定 URL 的主机。对于要镜像的 SSL 站点,该函数将其映射到 SSL 隧道,该隧道透明地将流量转发到实际服务器,如 RFC 2817 中给出的 CONNECT 方法的描述。通过这种方式,位于 http://127.0.0.1:8888 的单个 HTTP 服务器可以成功模拟位于不同位置的数十个服务器。

如果我的测试在 onload 触发时未完成怎么办?

在 onload 触发之前使用 add_task() 或调用 SimpleTest.waitForExplicitFinish()(并在完成后调用 SimpleTest.finish())。

如何在自动化中获取测试的完整日志输出以进行调试?

将以下内容添加到您的测试中

SimpleTest.requestCompleteLog();

如果需要更改首选项才能运行我的测试怎么办?

SpecialPowers 对象提供用于获取和设置首选项的 API

await SpecialPowers.pushPrefEnv({ set: [["your-preference", "your-value" ]] });
// ...
await SpecialPowers.popPrefEnv(); // Implicit at the end of the test too.

您也可以在清单中直接设置首选项

[DEFAULT]
prefs =
  browser.chrome.guess_favicon=true

如果需要在本地运行测试时更改首选项,可以使用 --setpref 标志

./mach mochitest --setpref="javascript.options.jit.chrome=false" somePath/someTestFile.html

同样,如果需要更改字符串首选项

./mach mochitest --setpref="webgl.osmesa=string with whitespace" somePath/someTestFile.html

要更改多个首选项,可以为每个首选项添加一个 --setpref 参数

./mach mochitest --setpref="some.boolpref=true" --setpref="some.stringpref=string with whitespace" somePath/someTestFile.html

测试是否可以在 chrome URL 下运行?

是的,请使用 mochitest-chrome

如何更改 Mochitest 中使用的文件发送的 HTTP 标头或状态?

在要修改其标头的文件旁边创建一个文本文件。文本文件的名称应为要修改其标头的文件的名称,后跟 ^headers^。例如,如果您有一个文件 foo.jpg,则文本文件的名称应为 foo.jpg^headers^。(不要尝试在测试中以任何其他方式实际使用标头文件,因为 HTTP 服务器的隐藏文件功能阻止任何以恰好一个 ^ 结尾的文件被提供服务。)

编辑文件以包含要设置的标头和/或状态,如下所示

HTTP 404 Not Found
Content-Type: text/html
Random-Header-of-Doom: 17

第一行设置 HTTP 状态和与文件关联的描述(可选)。此行是可选的;如果对正常的响应状态和描述感到满意,则不需要它。

文件中的任何其他行都描述了您想要添加或覆盖的附加标头(最典型的是 Content-Type 标头,对于后一种情况)在响应上。格式遵循 HTTP 的约定,除了您不需要具有 HTTP 行结尾并且您不能多次使用标头(特定标头的最后一行获胜)。文件最多可以以一个空行结尾以匹配 Unix 文本文件约定,但尾随换行符不是严格必需的。

如何编写检查 HTTP 请求的标头值、方法类型等的测试?

要编写这样的测试,您只需为其编写一个 SJS(服务器端 JavaScript)。有关您可以在 SJS 脚本中执行的操作的更多非 mochitest 特定文档,请参阅 测试 HTTP 服务器 文档。

SJS 只是一个扩展名为 .sjs 的 JavaScript 文件,它在沙箱中加载。不要忘记从您的 mochitest.ini 文件中引用它!

[DEFAULT]
support-files =
  test_file.sjs

脚本定义的全局属性 handleRequest 然后使用请求和响应对象执行,并且脚本根据请求中的信息填充响应。

这是一个简单的 SJS 示例

function handleRequest(request, response) {
  // Allow cross-origin, so you can XHR to it!
  response.setHeader("Access-Control-Allow-Origin", "*", false);
  // Avoid confusing cache behaviors
  response.setHeader("Cache-Control", "no-cache", false);
  response.setHeader("Content-Type", "text/plain", false);
  response.write("Hello world!");
}

例如,该文件在 http://mochi.test:8888/tests/PATH/TO/YOUR/test_file.sjs、http://{server-location}/tests/PATH/TO/YOUR/test_file.sjs 上运行 - 请参阅 build/pgo/server-locations.txt 以获取服务器位置!

如果要实际执行该文件,则需要以某种方式引用它。例如,您可以 XHR 到它,或者您可以使用 HTML 元素

var xhr = new XMLHttpRequest();
xhr.open("GET", "http://test/tests/dom/manifest/test/test_file.sjs");
xhr.onload = function(e){ console.log("loaded!", this.responseText)}
xhr.send();

请求和响应参数的确切属性在 nsIHttpRequestMetadatansIHttpResponse 接口中定义,在 nsIHttpServer.idl 中。但是,以下是一些有用的属性

  • .scheme(字符串)。请求的方案。

  • .host(字符串)。请求的方案。

  • .port(字符串)。请求的端口。

  • .method(字符串)。HTTP 方法。

  • .httpVersion(字符串)。协议版本,通常为“1.1”。

  • .path(字符串)。请求的路径,

  • .headers(对象)。表示标头的名称和值。

  • .queryString(字符串)。请求的 URL 的查询字符串。

  • .bodyInputStream ??

  • .getHeader(name)。按名称获取请求标头。

  • .hasHeader(name)(布尔值)。按名称获取请求标头。

**注意**:浏览器可以自由缓存脚本生成的响应。如果您希望 SJS 对同一 URL 的多个请求返回不同的数据,则应向响应中添加 Cache-Control: no-cache 标头以防止测试意外失败,尤其是在同一 Mochitest 会话中手动运行多次时。

如何跨多个不同的服务器端脚本保持状态?

Mochitest 中的服务器端脚本在沙箱内运行,每个新加载都会创建一个新的沙箱。因此,在处理程序中设置的任何变量都不会跨加载持久化。为了支持状态存储,请使用全局对象上定义的 getState(k)setState(k, v) 方法。这些方法公开了服务器的键值存储机制,键和值均为字符串。(使用 JSON 存储对象和其他结构化数据。)Mochitest 中的众多服务器实际上是一个具有某些代理和隧道功能的单个服务器,因此存储的状态在任何时间的所有服务器中都相同。

getStatesetState 方法的作用域是正在加载的路径。例如,绝对 URL /foo/bar/baz, /foo/bar/baz?quux, /foo/bar/baz#fnord 共享相同的状态;/foo/bar 的状态是完全独立的。

您应尽可能使用每路径状态,以避免测试间依赖关系和错误。

但是,在极少数情况下,两个脚本可能需要以某种方式协作,并且可能无法使用自定义查询字符串来请求脚本的不同行为。

仅针对此用例,您应使用全局对象上定义的 getSharedState(k, v)setSharedState(k, v) 方法。对整个服务器共享状态的访问没有限制,任何脚本都可以添加其他任何脚本可以删除的新状态。为了避免冲突,您应该在伪命名空间中使用键,以避免意外冲突。例如,如果您需要 HTML5 视频测试的共享状态,则可以使用类似 dom.media.video:sharedState 的键。

getObjectState(k)setObjectState(k, v) 方法提供了另一种状态存储形式,它们将存储任何 nsISupports 对象。这些方法位于 nsIHttpServer 接口上,但是服务器用于处理 SJS 响应的沙箱对象的限制意味着前者存在于 SJS 请求处理程序的全局环境中,签名为 getObjectState(k, callback),其中 callback 是一个函数,由 getObjectState 调用,并将与提供的键对应的对象作为唯一参数。

请注意,此值映射要求值是 XPCOM 对象;没有 QueryInterface 方法的任意 JavaScript 对象是不够的。如果您希望存储 JavaScript 对象,您可能会发现为对象提供 QueryInterface 实现很有用,然后利用 wrappedJSObject 通过 XPConnect 执行的包装来揭示实际的 JavaScript 对象。

有关 httpd.js 提供的状态保存机制的更多详细信息,请参阅 netwerk/test/httpserver/nsIHttpServer.idlnsIHttpServer.get(Shared|Object)?State 方法。

如何编写异步响应的 SJS 脚本?

有时您需要异步响应请求,例如在等待短时间后。您可以通过使用传递给 handleRequest() 函数的响应对象上的 processAsync()finish() 函数来做到这一点。

processAsync() 必须在从 handleRequest() 返回之前调用。调用后,您可以在任何时候调用请求对象上的方法来发送更多响应。完成后,调用 finish() 函数。例如,您可以使用上面描述的 setState() / getState() 函数来存储请求,并在以后检索并完成它。但是请注意,浏览器通常会重新排序请求,因此您的代码必须能够抵御这种情况,以避免间歇性故障。

let { setTimeout } = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");

function handleRequest(request, response) {
  response.processAsync();
  response.setHeader("Content-Type", "text/plain", false);
  response.write("hello...");

  setTimeout(function() {
    response.write("world!");
    response.finish();
  }, 5 * 1000);
}

有关更多详细信息,请参阅 netwerk/test/httpserver/nsIHttpServer.idl 中的 processAsync() 函数文档。

如何在 SJS 脚本中以 XPCOM 对象的形式访问服务器上的文件?

如果您需要访问文件(因为将图像数据存储在文件中比直接存储在 SJS 脚本中更容易),请使用 Mochitest 中运行的 SJS 脚本可用的预先提供的 SERVER_ROOT 对象状态。

function handleRequest(req, res) {
  var file;
  getObjectState("SERVER_ROOT", function(serverRoot) {
    file = serverRoot.getFile("tests/content/media/test/320x240.webm");
  });
  // file is now an XPCOM object referring to the given file
  res.write("file: " + file);
}

您指定的路径用作 httpd.js 提供服务的根目录的相对路径,并返回对应于该位置文件的 nsIFile

注意错别字:您指定的文件实际上不必存在,因为文件对象仅仅是字符串路径的封装。

诊断和修复 leakcheck 失败

在调试版本中,Mochitests 会输出在测试期间创建的窗口和 docshell 的日志。在测试结束时,测试运行程序会运行 leakcheck 分析以确定在测试结束前是否有任何窗口或 docshell 未被清理。

泄漏可能由于各种原因发生。一个常见的原因是 JavaScript 事件监听器保留了一个使窗口保持活动的引用。

// Add an observer.
Services.obs.addObserver(myObserver, "event-name");

// Make sure and clean it up, or it may leak!
Services.obs.removeObserver(myObserver, "event-name");

其他问题来源包括意外地将窗口或 iframe 附加到 DOM,或将 iframe 的 src 设置为空字符串(创建 about:blank 页面),而不是删除 iframe。

查找泄漏可能很困难,但第一步是在本地重现它。确保您使用的是调试版本,并且未启用 MOZ_QUIET 环境标志。leakcheck 测试会分析测试输出。在测试中重现泄漏后,开始注释掉代码,直到泄漏消失。然后,一旦泄漏停止重现,找到发生泄漏的确切位置。

有关涉及 CC 和 GC 日志的更高级的调试技术,请参阅这篇博文

如何运行辅助功能测试 (a11y 检查)?

可以使用 --enable-a11y-checks 标志在本地运行辅助功能测试。

./mach mochitest --enable-a11y-checks somePath/someTestFile.html

在 CI 上,a11y 检查仅在第 2 层 Linux 18.04 x64 WebRender(Opt 和 Shippable)构建上运行。如果您只想在 Try 上运行 a11y 检查,则可以使用 ./mach try fuzzy --full 命令,并使用查询 a11y-checks linux !wayland !tsan !asan !ccov !debug !devedition 进行所有检查。或者,要排除 devtools chrome 测试,请将查询 swr-a11y-checks 传递给 ./mach try fuzzy --full

如果您对 a11y 检查的结果以及解决任何问题的方法有任何疑问,请联系辅助功能团队,#accessibility 房间在 Matrix 上

如何调试失败的辅助功能测试 (a11y 检查)?

首先,查看失败日志以了解哪些元素可能无法访问。例如

TEST-UNEXPECTED-FAIL | path/to/specific/test/browser_test.js | Node is not accessible via accessibility API: id: foo, tagName: div, className: bar

此错误报告指出,当 browser_test.js 运行时,单击了一个 <div id="foo" class="bar">,并且此元素可能无法访问。下一步是查看此元素的代码,以确保它被构建为可访问的。

  1. 具有交互角色

  2. 已启用

  3. 可以通过键盘获得焦点

  4. 已标记

当上述所有条件都为真时,该元素可以单击

之后,检查该元素实际上是可见的,并且在测试单击它时是可访问的。例如,在处理弹出面板时,有时可能需要等待面板显示,然后才能单击其中的按钮。

如果问题不清楚,请随时联系辅助功能团队一起集思广益。

我的补丁导致 a11y 检查失败。我现在可以跳过它们吗?

通常,我们只在出现无法解释的间歇性错误或崩溃时跳过 a11y 检查(示例)。在我们的存储库中,此类测试很少。

如果出现故障且解决方案尚不清楚,或者如果故障排除需要单独的补丁,请确保提交后续错误并将其引用,同时在测试清单中设置 fail-if示例)。这允许辅助功能测试在将来可能添加的任何新 UI 上继续运行并在日志中报告,即使没有第 2 层错误标记。然后,当问题得到修复并且测试通过时,您可以从测试清单中删除 fail-if 符号。如果您忘记删除 fail-if 符号,系统会通知您一个(意外)通过的测试,以提醒您更新清单。

a11y 检查的例外情况是什么?我应该何时以及如何实现它们?

有时,预期测试用例会使辅助功能实用程序测试失败。例如,当 mochitest 单击禁用按钮以确认实际上没有任何事情发生时。在这些情况下,您可以有意修改默认测试环境对象以禁用特定检查。您可以通过在单击之前调用 AccessibilityUtils.setEnv,然后在单击之后将其重置为原始状态来执行此操作(以避免无意中排除其他测试用例)。

AccessibilityUtils.setEnv 调用通常应该以描述性注释开头。这有助于避免无意中将此代码复制粘贴到新的测试用例中,从而导致辅助功能回归。请参阅下面最常见的异常情况中链接的示例,了解示例注释。

单击禁用控件或其他非交互式 UI 以确认单击事件不会发生

报告的错误为:Node expected to be enabled but is disabled via the accessibility API

这些单击并非旨在进行交互,并且它们的 target 不应通过辅助功能 API 访问或启用。因此,我们通过 AccessibilityUtils.setEnv({ mustBeEnabled: false }) 为此测试添加了一个 a11y 检查例外(示例)。

单击非交互式 UI 以确认没有任何事情发生

报告的错误为:Node is not accessible via accessibility API

这些单击并非旨在进行交互,并且它们的 target 不应通过辅助功能 API 访问或可用。因此,我们通过 AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }) 为此测试添加了一个 a11y 检查例外(示例)。

单击任意 Web 内容和远程文档

报告的错误为:Node is not accessible via accessibility API

我们不想测试远程 Web 内容,因为我们目前不支持使用 a11y 检查的远程文档。此外,我们认识到某些任意远程内容是为练习特定浏览器功能而编写的,因此不需要完整。因此,我们通过 AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }) 为此测试添加了一个 a11y 检查例外(示例)。

单击非交互式内容以关闭对话框/菜单弹出窗口/面板

报告的错误为:Node is not accessible via accessibility API

某些测试会向非交互式元素(例如,容器、页面的 <body> 等)发送单击事件,以关闭打开的对话框、菜单弹出窗口或面板。虽然这种交互方式对于某些用户来说是不可访问的,但只要有其他可访问的方式供键盘和辅助技术用户关闭弹出窗口,例如按 Esc 键或 X 关闭按钮,它就是可以接受的。在这种情况下,我们需要通过 AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }) 为此测试添加一个 a11y 检查例外,同时在注释中明确说明至少存在另一种不依赖于鼠标的方法来关闭它(单击示例单击间隔符示例)。

从不期望最终用户执行的非用户界面测试用例(例如,遥测、性能、崩溃测试)

报告的错误为:Node is not accessible via accessibility API

有时,我们会测试从不期望真实用户执行的行为;例如,确认崩溃测试补丁是否有效,或单击某个元素以测试遥测或性能操作。只要我们有其他测试检查同一 UI 的辅助功能,您就可以通过 AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }) 为此测试添加一个 a11y 检查例外,同时在注释中明确说明排除此测试的原因(性能测试单击示例崩溃测试示例遥测行为测试示例 以及 单击隐藏面板以确认其内容已刷新的示例)。