Puppeteer 宣布对 Firefox 官方支持

我们很高兴地宣布,从第 23 版开始,Puppeteer 浏览器自动化库现在对 Firefox 提供了一流的支持。这意味着,现在可以轻松地使用 Puppeteer 进行自动化和端到端测试,并在 ChromeFirefox 上运行。

如何在 Firefox 上使用 Puppeteer

要开始使用,只需在启动 Puppeteer 时将 product 设置为 “firefox”:

1
2
3
4
5
6
7
8
9
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
browser: "firefox"
});

const page = await browser.newPage();
// ...
await browser.close();

Chrome 一样,Puppeteer 能够下载并启动最新的Firefox稳定版本,因此在任一浏览器上运行应能提供 Puppeteer 用户所期待的相同开发体验。

虽然 Puppeteer 提供的功能并不令人意外,但为多个浏览器提供支持是一个重大的工程。Firefox 支持并不是基于特定于 Firefox 的自动化协议,而是基于 WebDriver BiDi,这是一个正在 W3C 进行标准化的跨浏览器协议,目前已在 GeckoChromium中实现。使用这种跨浏览器协议应该会使未来支持多种不同的浏览器变得更加容易。

在本文的后面部分,我们将深入探讨 WebDriver BiDi 背后的一些更技术的背景。但首先,我们要指出,今天的公告是如何通过富有成效的合作来推进 Web 技术前沿的一个很好例证。开发一个新的浏览器自动化协议需要大量工作,特别感谢 Puppeteer 团队和 W3C 浏览器测试与工具工作组的其他成员,他们为我们达到这一点所付出的所有努力。

你还可以查看 Puppeteer 团队关于使 WebDriver BiDi 达到生产准备状态的帖子。

主要功能

对于长期的 Puppeteer 用户来说,这些功能已经很熟悉了。然而,对于其他自动化和测试生态系统中的人,特别是那些直到最近还完全依赖于基于 HTTP 的 WebDriver 的人来说,本节概述了 WebDriver BiDi 在跨浏览器方式中实现的新功能。

捕获日志消息

在测试 Web 应用时,一个常见的需求是确保没有意外的错误报告到控制台。这也是事件驱动协议闪耀的地方,因为它避免了需要轮询浏览器以获取新的日志消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
browser: "firefox"
});

const page = await browser.newPage();
page.on('console', msg => {
console.log(`[console] ${msg.type()}: ${msg.text()}`);
});

await page.evaluate(() => console.debug('Some Info'));
await browser.close();

输出:

1
[console] debug: Some Info

设备模拟

在测试响应式布局时,通常需要确保布局在多个屏幕尺寸和设备像素比下都能正常工作。这可以通过使用真实的移动浏览器在设备或模拟器上完成。然而,为了简单起见,可以在桌面设备上模拟移动设备的视口进行测试。下面的示例展示了如何在配置为模拟 Pixel 5 手机视口大小和设备像素比的 Firefox 中加载一个页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import puppeteer from "puppeteer";

const device = puppeteer.KnownDevices["Pixel 5"];

const browser = await puppeteer.launch({
browser: "firefox"
});

const page = await browser.newPage();
await page.emulate(device);

const viewport = page.viewport();

console.log(
`[emulate] Pixel 5: ${viewport.width}x${viewport.height}` +
` (dpr=${viewport.deviceScaleFactor}, mobile=${viewport.isMobile})`
);

await page.goto("https://www.mozilla.org");
await browser.close();

输出:

1
[emulate] Pixel 5: 393x851 (dpr=3, mobile=true)

网络拦截

测试中的一个常见需求是能够跟踪和拦截网络请求。拦截特别有用,可以避免测试期间发送到第三方服务的请求,并提供模拟的响应数据。它还可以用于处理 HTTP 身份验证对话框,覆盖请求和响应的部分内容,例如添加或删除标头。在下面的示例中,我们使用网络请求拦截来阻止页面上所有的 Web 字体请求,这可能有助于确保这些字体加载失败时不会破坏网站布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
browser: 'firefox'
});

const page = await browser.newPage();
await page.setRequestInterception(true);

page.on("request", request => {
if (request.url().includes(".woff2")) {
// 阻止自定义用户字体的请求。
console.log(`[intercept] Request aborted: ${request.url()}`);
request.abort();
} else {
request.continue();
}
});

const response = await page.goto("https://support.mozilla.org");
console.log(
`[navigate] status=${response.status()} url=${response.url()}`
);
await browser.close();

输出:

1
2
[intercept] Request aborted: https://assets-prod.sumo.prod.webservices.mozgcp.net/static/Inter-Bold.3717db0be15085ac.woff2
[navigate] status=200 url=https://support.mozilla.org/en-US/

预加载脚本

通常,自动化工具希望提供可以用 JavaScript 实现的自定义功能。虽然 WebDriver 一直允许注入脚本,但无法确保注入的脚本总是在页面开始加载之前运行,从而无法避免页面脚本与注入脚本之间的竞争。

WebDriver BiDi 提供了“预加载”脚本,可以在页面加载之前运行。它还提供了一种从脚本中发出自定义事件的方法。例如,这可以避免为预期元素轮询,而是使用一旦元素可用就触发的变更观察器。下面的示例中,我们等待页面上的 <title> 元素出现,并记录其内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
browser: 'firefox',
});

const page = await browser.newPage();

const gotMessage = new Promise(resolve =>
page.exposeFunction("sendMessage", async message => {
console.log(`[script] Message from pre-load script: ${message}`);
resolve();
})
);

await page.evaluateOnNewDocument(() => {
const observer = new MutationObserver(mutationList => {
for (const mutation of mutationList) {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
if (node.tagName === "TITLE") {
sendMessage(node.textContent);
}
}
}
};
});

observer.observe(document.documentElement, {
subtree: true,
childList: true,
});
});

await page.goto("https://support.mozilla.org");
await gotMessage;
await browser.close();

输出:

1
[script] Message from pre-load script: Mozilla Support

技术背景

直到最近,想要自动化浏览器的人有两个主要选择:

    1. 使用基于 Selenium 项目早期工作的 W3C WebDriver API。
    1. 使用特定于浏览器的 API 与每个支持的浏览器进行通信,例如用于基于 Chromium 的浏览器的 Chrome DevTools Protocol (CDP) 或用于基于 Gecko 的浏览器的 Firefox 远程调试协议 (RDP)。

不幸的是,这两种选择都存在显著的权衡。经典的 WebDriver API 是基于 HTTP 的,它的模型是自动化进程向浏览器发送命令并等待响应。这对于加载页面然后验证某些元素是否显示等自动化场景效果良好,但无法从浏览器获取事件(例如控制台日志)或同时运行多个命令,使得该 API 不适合更高级的用例。

相比之下,特定于浏览器的 API 通常是围绕支持浏览器开发工具的复杂用例而设计的。因此,它们的功能集远超 WebDriver 的可能性,因为它们需要支持诸如记录控制台日志或网络请求等用例。

因此,浏览器自动化客户端不得不在使用单一协议支持多种浏览器并提供有限的功能集与提供丰富的功能集但需要为每个支持的浏览器分别实现多个协议之间做出选择。这显然增加了创建出色的跨浏览器自动化的成本和复杂性,这对开发者来说并不理想,特别是当开发者普遍将跨浏览器测试视为 Web 开发的主要痛点之一时。

长期开发者可能会注意到,这种情况类似于在 Language Server Protocol (LSP) 开发之前编辑器的情况。当时,每个文本编辑器或 IDE 都必须为每种不同的编程语言实现定制的支持。这使得很难在所有开发者使用的工具中获得对一种新语言的支持。LSP 的出现改变了这一切,它提供了一个可以被任何编辑器和编程语言组合支持的通用协议。对于像 TypeScript 这样的新编程语言来说,它不再需要让每个编辑器一个一个地添加支持;它只需要提供一个 LSP 服务器,它就会自动在所有支持 LSP 的编辑器中得到支持。这一通用协议的出现还使得以前难以想象的事情成为可能。例如,像 Tailwind 这样的特定库现在有了自己的 LSP 实现,从而实现了定制的编辑器功能。

为了改善跨浏览器自动化,我们采取了类似的方法:开发 WebDriver BiDi,将之前仅限于特定浏览器协议的自动化功能集引入到一个标准化协议中,任何浏览器都可以实现,任何编程语言中的自动化工具都可以使用。

在 Mozilla,我们认为标准化协议以消除进入壁垒,允许多样化的互操作实现生态系统蓬勃发展,并使用户能够选择最适合其需求的方案,这是我们宣言和 Web 愿景的关键部分。

有关 WebDriver BiDi 的设计及其与经典 WebDriver 关系的更多详细信息,请参阅我们之前的帖子。

删除 Firefox 中的实验性 CDP 支持

作为我们改进跨浏览器测试的早期工作的一部分,我们发布了一个部分实现的 CDP,限于支持测试用例所需的少数命令和事件。这曾是 Puppeteer 对 Firefox 实验性支持的基础。然而,一旦显然这不是跨浏览器自动化的未来之路,工作就停止了。因此,它没有得到维护,也不适用于现代 Firefox 功能,例如站点隔离。因此,计划在 2024 年底删除支持。

如果您当前在 Firefox 中使用 CDP,不知道如何过渡到 WebDriver BiDi,请通过本文底部列出的渠道之一联系我们,我们会讨论您的需求。

接下来是什么?

虽然 Firefox 现在已正式支持 Puppeteer,并且具有足够的功能来涵盖许多自动化和测试场景,但仍有一些 API 未得到支持。这些大致分为三类(请查阅 Puppeteer 文档以获取完整列表):

  1. 高度 CDP 特定的 API,尤其是 CDPSession 模块中的那些。这些 API 不太可能被直接支持,但目前需要这些 API 的特定用例可能会成为标准化的候选对象。
  2. 需要进一步标准化的 API。例如,page.accessibility.snapshot 返回 Chromium 无障碍树的转储。然而,由于目前没有标准化的描述来说明该树应该是什么样子,因此很难以跨浏览器的方式进行工作。还有一些更为简单的情况,只需要对 WebDriver BiDi 规范本身进行工作,例如 page.setGeolocation
  3. 已经有标准但尚未实现的 API,例如执行命令所需的在 worker 中执行脚本的能力,如 WebWorker.evaluate

我们预计将来会填补这些空白。为了帮助确定优先级,我们很感兴趣听取您的反馈:请尝试在 Firefox 中运行您的 Puppeteer 测试!如果因为某个 bug 或缺失的功能而无法在 Firefox 中运行,请使用下面列出的方式之一让我们知道,以便我们在规划未来的标准化和实现工作时考虑这些问题:

  • 对于 Firefox 实现中的错误,请在 Bugzilla 上提交错误报告。
  • 如果您确信问题出在 Puppeteer 中,请在他们的问题跟踪器中提交错误报告。
  • 对于 WebDriver BiDi 规范中缺少的功能,请在 GitHub 上提交问题。
  • 如果您想与我们讨论用例或需求,请使用 Mozilla 的 Matrix 实例上的 #webdriver 频道,或发送电子邮件至 dev-webdriver@mozilla.org