使用 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": "^24.8.1"
  },
  "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,
    pipe: true,
    enableExtensions: [EXTENSION_PATH]
  });
});

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

请注意,我们的测试从示例代码库加载了 test-extension。 browser.runtime.onMessage 的处理程序依赖于 browser.runtime.onInstalled 事件的处理程序中设置的状态。因此,当 Service Worker 终止时,data 的内容将丢失,并且无法响应任何未来的消息。我们会在编写测试后修复此问题。

service-worker-broken.js:

let data;

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

browser.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();
}

最后,使用以下代码更新测试。现在,终止服务工作线程,然后再次点击该按钮,检查您是否收到了响应。

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-fixed.js 中的以下代码。

service-worker-fixed.js:

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

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

在此示例中,我们将版本保存到 browser.storage.local 而不是全局变量,以便在服务工作线程生命周期之间保持状态。由于只能异步访问存储空间,我们还会从 onMessage 监听器返回 true,以确保 sendResponse 回调保持有效。

第 7 步:再次运行测试

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

后续步骤

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

  • 构建测试套件以支持在发生或未发生意外的服务工作线程终止的情况下运行。然后,您可以单独运行这两种模式,以便更清楚地了解导致失败的原因。
  • 编写代码以在测试中随机终止 Service Worker。这有助于发现可能难以预测的问题。
  • 从测试失败中吸取经验教训,并尝试在未来编写防御性代码。例如,添加一个 linting 规则来阻止使用全局变量,并尝试将数据移到更持久的状态。