宣布 Firefox 正式支持 Puppeteer

我们很高兴地宣布,从 23 版开始,Puppeteer 浏览器自动化库现在正式支持 Firefox。这意味着现在可以使用 Puppeteer 很容易地编写自动化和执行端到端测试,并且可以在 Chrome 和 Firefox 上运行。

如何在 Firefox 中使用 Puppeteer

要开始使用,只需在启动 Puppeteer 时将产品设置为“firefox

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 标准化,目前已在 Gecko 和 Chromium 中实现。使用跨浏览器协议应该可以更轻松地在将来支持许多不同的浏览器。

在本帖的后面,我们将深入探讨 WebDriver BiDi 背后的一些更技术性的背景。但首先,我们想指出今天的公告很好地证明了如何通过富有成效的合作来推动网络技术发展。开发新的浏览器自动化协议需要大量工作,对 Puppeteer 团队以及 W3C 浏览器测试和工具工作组的其他成员为将我们带到今天这一步所付出的所有努力表示衷心的感谢。

您还可以查看 Puppeteer 团队的帖子,了解如何使 WebDriver BiDi 投入生产环境。

主要功能

对于长期使用 Puppeteer 的用户来说,可用的功能是熟悉的。但是对于其他自动化和测试生态系统中的人来说,尤其是那些直到最近仍然完全依赖于基于 HTTP 的 WebDriver 的人,本节概述了 WebDriver BiDi 使得跨浏览器实现的一些新功能。

捕获日志消息

测试 Web 应用程序时的一项常见要求是确保没有向控制台报告意外错误。这也是基于事件的协议大放异彩的地方,因为它避免了需要轮询浏览器以获取新的日志消息。

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();

输出

[console] debug: Some Info

设备模拟

在测试响应式布局时,通常很有用的是能够确保布局在多个屏幕尺寸和设备像素比上都能正常工作。这可以通过使用真实的移动浏览器来完成,无论是设备上还是模拟器上。但是为了简便起见,可以在桌面设置上进行测试,以模拟移动设备的视窗。以下示例显示了在配置为模拟 Pixel 5 手机的视窗大小和设备像素比的 Firefox 中加载页面。

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();

输出

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

网络拦截

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

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")) {
    // Block requests to custom user fonts.
    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();

输出

[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>元素出现在页面上,并记录其内容。

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();

输出

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

技术背景

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

  • 使用 W3C WebDriver API,该 API 基于 Selenium 项目的早期工作。
  • 使用与每个支持的浏览器进行通信的特定于浏览器的 API,例如 Chrome DevTools Protocol (CDP) 用于基于 Chromium 的浏览器,或 Firefox 的 Remote Debugging Protocol (RDP) 用于基于 Gecko 的浏览器。

不幸的是,这两种选择都有很大的权衡。传统的 WebDriver API 是基于 HTTP 的,其模型涉及自动化向浏览器发送命令并等待响应。这在您加载页面然后验证例如某个元素是否显示的自动化场景中很有效,但是无法从浏览器获取事件(例如控制台日志),或者无法并发运行多个命令,这使得 API 成为更高级用例的糟糕选择。

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

因此,浏览器自动化客户端不得不做出选择:使用单个协议支持许多浏览器并提供有限的功能集,或者提供更丰富的功能集,但必须实现多个协议以便分别为每个支持的浏览器提供功能。这显然增加了创建出色的跨浏览器自动化的成本和复杂性,这不是一件好事,尤其是在开发人员经常提到跨浏览器测试是 Web 开发的主要痛点之一的时候。

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

因此,为了改进跨浏览器自动化,我们采取了类似的方法:开发 WebDriver BiDi,它将以前仅限于特定于浏览器的协议的自动化功能集带到标准化协议中,该协议可以由任何浏览器实现并由任何编程语言中的任何自动化工具使用。

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

有关 WebDriver BiDi 的设计以及它如何与传统 WebDriver 相关联的更多详细信息,请参阅我们早期 帖子

在 Firefox 中删除实验性 CDP 支持

作为我们改进跨浏览器测试的早期工作的一部分,我们发布了 CDP 的部分实现,仅限于支持测试用例所需的少数命令和事件。这以前是 Puppeteer 中对 Firefox 的实验性支持的基础。但是,一旦明确这不是跨浏览器自动化的前进方向,这项工作的努力就停止了。因此,它没有得到维护,并且无法与现代 Firefox 功能(如站点隔离)一起使用。因此,支持将在 2024 年底被移除

如果您目前正在使用 CDP 与 Firefox,并且不知道如何迁移到 WebDriver BiDi,请使用 本文底部列出的渠道之一与我们联系,我们将讨论您的需求。

下一步是什么?

尽管 Firefox 现在已在 Puppeteer 中获得官方支持,并且具有足够的功​​能来涵盖许多自动化和测试场景,但仍有一些 API 尚未得到支持。这些大致分为三类(请参阅 Puppeteer 文档 获取完整列表)

  • 高度特定于 CDP 的 API,尤其是 CDPSession 模块中的 API。这些 API 不太可能直接得到支持,但目前需要这些 API 的特定用例可能是标准化的候选者。
  • 需要进一步标准化工作的 API。例如,page.accessibility.snapshot 返回 Chromium 可访问性树的转储。但是,由于目前还没有对该树应该是什么样子的标准化描述,因此很难以跨浏览器的方式使其工作。还有一些情况要简单得多,因为它们只需要对 WebDriver BiDi 规范本身进行工作;例如,page.setGeolocation
  • 具有标准但尚未实现的 API,例如在工作线程中执行脚本的能力,这是 WebWorker.evaluate 等命令所需的。

我们希望在未来弥合这些差距。为了帮助确定优先级,我们希望获得您的反馈:请尝试在 Firefox 中运行您的 Puppeteer 测试!如果您无法在 Firefox 中运行它们是因为 bug 或缺少功能,请使用以下方法之一告知我们,以便我们可以在规划未来标准和实现工作时将其考虑在内。

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

关于 James Graham

专注于维护健康的开放网络的软件工程师。Web 平台测试核心团队成员。

James Graham 的更多文章…

关于 Henrik Skupin

Henrik Skupin 的更多文章…

关于 Julian Descottes

Julian Descottes 的更多文章…

关于 Alexandra Borovova

Alexandra Borovova 的更多文章…