使用 Puppeteer 测试 Service Worker 的终止情况

本指南介绍了如何通过使用 Puppeteer 测试 Service Worker 的终止来构建更强大的扩展程序。准备好随时应对终止情况非常重要,因为这可能会在没有警告的情况下发生,导致 Service Worker 中的任何非持久性状态丢失。因此,在有要处理的事件时,扩展程序必须保存重要状态,并且能够在再次启动后立即处理请求。

前期准备

克隆或下载 chrome-extensions-samples 代码库。 我们将在 /functional-samples/tutorial.terminate-sw/test-extension 中使用该测试扩展程序,该扩展程序会在用户点击按钮时向 Service Worker 发送消息,并在收到响应时向页面添加文本。

您还需要安装 Node.JS,这是 Puppeteer 所基于的运行时。

第 1 步:启动 Node.js 项目

在新目录中创建以下文件。二者一起创建一个新的 Node.js 项目,并提供使用 Jest 作为测试运行程序的 Puppeteer 测试套件的基本结构。如需详细了解此设置,请参阅使用 Puppeteer 测试 Chrome 扩展程序

package.json:

{
  "name": "puppeteer-demo",
  "version": "1.0",
  "dependencies": {
    "jest": "^29.7.0",
    "puppeteer": "^22.1.0"
  },
  "scripts": {
    "start": "jest ."
  },
  "devDependencies": {
    "@jest/globals": "^29.7.0"
  }
}

index.test.js:

const puppeteer = require('puppeteer');

const SAMPLES_REPO_PATH = 'PATH_TO_SAMPLES_REPOSITORY';
const EXTENSION_PATH = `${SAMPLES_REPO_PATH}/functional-samples/tutorial.terminate-sw/test-extension`;
const EXTENSION_ID = 'gjgkofgpcmpfpggbgjgdfaaifcmoklbl';

let browser;

beforeEach(async () => {
  browser = await puppeteer.launch({
    // Set to 'new' to hide Chrome if running as part of an automated build.
    headless: false,
    args: [
      `--disable-extensions-except=${EXTENSION_PATH}`,
      `--load-extension=${EXTENSION_PATH}`
    ]
  });
});

afterEach(async () => {
  await browser.close();
  browser = undefined;
});

请注意,我们的测试会从示例仓库加载 test-extensionchrome.runtime.onMessage 的处理程序依赖于在 chrome.runtime.onInstalled 事件的处理程序中设置的状态。因此,当 Service Worker 终止并且对将来的任何消息的响应失败时,data 的内容将会丢失。我们将在编写测试后解决此问题。

service-worker-broken.js:

let data;

chrome.runtime.onInstalled.addListener(() => {
  data = { version: chrome.runtime.getManifest().version };
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  sendResponse(data.version);
});

第 2 步:安装依赖项

运行 npm install 以安装所需的依赖项。

第 3 步:编写基本测试

将以下测试添加到 index.test.js 底部。这会从我们的测试扩展程序中打开测试页面,点击按钮元素,然后等待 Service Worker 的响应。

test('can message service worker', async () => {
  const page = await browser.newPage();
  await page.goto(`chrome-extension://${EXTENSION_ID}/page.html`);

  // Message without terminating service worker
  await page.click('button');
  await page.waitForSelector('#response-0');
});

您可以使用 npm start 运行测试,您应该会看到它已成功完成。

第 4 步:终止 Service Worker

添加以下辅助函数来终止 Service Worker:

/**
 * Stops the service worker associated with a given extension ID. This is done
 * by creating a new Chrome DevTools Protocol session, finding the target ID
 * associated with the worker and running the Target.closeTarget command.
 *
 * @param {Page} browser Browser instance
 * @param {string} extensionId Extension ID of worker to terminate
 */
async function stopServiceWorker(browser, extensionId) {
  const host = `chrome-extension://${extensionId}`;

  const target = await browser.waitForTarget((t) => {
    return t.type() === 'service_worker' && t.url().startsWith(host);
  });

  const worker = await target.worker();
  await worker.close();
}

最后,使用以下代码更新您的测试。现在终止 Service Worker,并再次点击该按钮以检查您是否收到响应。

test('can message service worker when terminated', async () => {
  const page = await browser.newPage();
  await page.goto(`chrome-extension://${EXTENSION_ID}/page.html`);

  // Message without terminating service worker
  await page.click('button');
  await page.waitForSelector('#response-0');

  // Terminate service worker
  await stopServiceWorker(page, EXTENSION_ID);

  // Try to send another message
  await page.click('button');
  await page.waitForSelector('#response-1');
});

第 5 步:运行测试

运行 npm start。测试应该会失败,这表示 Service Worker 在终止后没有响应。

第 6 步:修复 Service Worker

接下来,通过取消对临时状态的依赖来修复 Service Worker。请更新测试扩展程序以使用存储在代码库中的 service-worker-fixed.js 中的以下代码。

service-worker-fixed.js:

chrome.runtime.onInstalled.addListener(() => {
  chrome.storage.local.set({ version: chrome.runtime.getManifest().version });
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  chrome.storage.local.get('version').then((data) => {
    sendResponse(data.version);
  });
  return true;
});

在这里,我们将版本保存到 chrome.storage.local 而不是全局变量,以在 Service Worker 的生命周期之间保留状态。由于只能异步访问存储空间,因此我们还会从 onMessage 监听器返回 true,以确保 sendResponse 回调保持活动状态。

第 7 步:再次运行测试

使用 npm start 再次运行测试。现在应该会通过。

后续步骤

现在,您可以将相同的方法应用于自己的扩展程序。请考虑以下事项:

  • 构建测试套件,以支持无论 Service Worker 是否意外终止时都能正常运行。然后,您可以分别运行这两种模式,以便更清楚地了解导致故障的原因。
  • 编写代码以在测试中的随机点终止 Service Worker。 此方法有助于发现可能难以预测的问题。
  • 从测试失败中汲取经验,在未来尝试防御性编码。例如,添加 lint 规则以阻止使用全局变量,并尝试将数据移至更持久的状态。