使用 Puppeteer 測試 Service Worker 終止事宜

本指南說明如何使用 Puppeteer 測試服務工作站終止情形,建構更強大的擴充功能。請務必做好隨時處理終止的準備,因為這項操作可在沒有警告的情況下發生,導致服務工作站中的任何非永久狀態遺失。因此,擴充功能必須儲存重要狀態,並能夠在有可處理的事件時,在再次啟動時處理要求。

事前準備

複製或下載 chrome-extensions-samples 存放區。我們會使用 /functional-samples/tutorial.terminate-sw/test-extension 中的測試擴充功能,在每次點選按鈕時,將訊息傳送至服務工作站,並在收到回應時將文字新增至頁面。

您還需要安裝 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();
}

最後,使用下列程式碼更新測試。接著終止服務工作站,然後再次按一下按鈕,檢查您是否已收到回應。

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

接著,移除對暫時狀態的依賴,藉此修正服務 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。 這樣有助於找出難以預測的問題。
  • 從測試失敗中學習,並嘗試在未來採用防禦性程式碼。舉例來說,您可以新增程式碼檢查規則,避免使用全域變數,並嘗試將資料移至更持續性狀態。