בדיקת סיום של קובץ שירות (service worker) באמצעות Puppeteer

במדריך הזה נסביר איך לפתח תוספים חזקים יותר על ידי בדיקת סיום הפעילות של עובדי השירות באמצעות Puppeteer. חשוב להיות מוכנים לטפל בסיום הפעילות בכל שלב, כי זה יכול לקרות ללא אזהרה, וכתוצאה מכך כל מצב לא עקבי ב-service worker יאבד. לכן, התוספים חייבים לשמור את המצב החשוב שלהם ולהיות מסוגלים לטפל בבקשות ברגע שהם מופעלים מחדש, כשיש אירוע שצריך לטפל בו.

לפני שמתחילים

מעתיקים (clone) או מורידים את המאגר chrome-extensions-samples. נשתמש בתוסף הבדיקה /functional-samples/tutorial.terminate-sw/test-extension כדי לשלוח הודעה ל-Service Worker בכל פעם שלוחצים על לחצן ומוסיף טקסט לדף אם מתקבלת תשובה.

תצטרכו גם להתקין את Node.JS, סביבת זמן הריצה שעליו מבוססת Puppeteer.

שלב 1: התחלת הפרויקט ב-Node.js

צריך ליצור את הקבצים הבאים בספרייה חדשה. יחד הם יוצרים פרויקט חדש ב-Node.js ומספקים את המבנה הבסיסי של חבילת בדיקות Puppeteer באמצעות Jest ככלי להרצת בדיקות. למידע נוסף על ההגדרה הזו, אפשר לעיין במאמר בדיקת תוספים ל-Chrome באמצעות Puppeteer.

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-extension ממאגר הדוגמאות. הטיפול באירוע chrome.runtime.onMessage מסתמך על המצב שהוגדר בטיפול באירוע chrome.runtime.onInstalled. כתוצאה מכך, התוכן של data ילך לאיבוד כשה-service worker יסתיים, והתגובה להודעות עתידיות תיכשל. נתקן את הבעיה אחרי שנכתב את הבדיקה שלנו.

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

בשלב הבא, מתקנים את ה-service worker על ידי הסרת התלות שלו במצב זמני. מעדכנים את test-extension כך שישתמש בקוד הבא, שמאוחסן ב-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 במקום במשתנה גלובלי כדי לשמור על המצב בין משך החיים של קובץ השירות. מכיוון שאפשר לגשת לאחסון רק באופן אסינכרוני, אנחנו גם מחזירים את הערך true מהמאזין onMessage כדי לוודא שהקריאה החוזרת (callback) של sendResponse תישאר פעילה.

שלב 7: מריצים שוב את הבדיקה

מריצים את הבדיקה שוב עם npm start. עכשיו היא אמורה לעבור.

השלבים הבאים

עכשיו אפשר להחיל את אותה גישה על התוסף שלכם. כדאי להביא בחשבון את הבאים:

  • כדאי לבנות את חבילת הבדיקות כדי שתתמוך בהרצה עם שירות לא צפוי או בלעדיו פיטורין של עובדים. לאחר מכן אפשר להריץ את שני המצבים בנפרד כדי שיהיה ברור יותר מה גרם לכשל.
  • כתיבת קוד לסיום של Service Worker בנקודות אקראיות בבדיקה. זו דרך טובה לגלות בעיות שקשה לחזות מראש.
  • לומדים מכישלונות בבדיקה ומנסים לכתוב קוד באופן הגנתי בעתיד. עבור למשל, הוסיפו כלל איתור שגיאות בקוד כדי להימנע משימוש במשתנים גלובליים ולנסות להעביר נתונים למצב קבוע יותר.