Tester l'arrêt d'un service worker avec Puppeteer

Ce guide explique comment créer des extensions plus robustes en testant l'arrêt des service workers à l'aide de Puppeteer. Il est important d'être prêt à gérer l'arrêt à tout moment, car cela peut se produire sans avertissement, ce qui entraîne la perte de tout état non persistant dans le service worker. Par conséquent, les extensions doivent enregistrer les états importants et être en mesure de traiter les requêtes dès qu'elles sont redémarrées lorsqu'un événement doit être géré.

Avant de commencer

Clonez ou téléchargez le dépôt chrome-extensions-samples. Nous allons utiliser l'extension de test dans /functional-samples/tutorial.terminate-sw/test-extension, qui envoie un message au service worker chaque fois qu'un bouton est cliqué et ajoute du texte à la page si une réponse est reçue.

Vous devrez également installer Node.JS, qui est l'environnement d'exécution sur lequel Puppeteer est basé.

Étape 1 : Démarrez votre projet Node.js

Créez les fichiers suivants dans un nouveau répertoire. Ensemble, ils créent un projet Node.js et fournissent la structure de base d'une suite de tests Puppeteer à l'aide de Jest comme exécuteur de test. Pour en savoir plus sur cette configuration, consultez Tester les extensions Chrome avec Puppeteer.

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

Notez que notre test charge le test-extension à partir du dépôt d'exemples. Le gestionnaire de browser.runtime.onMessage s'appuie sur l'état défini dans le gestionnaire de l'événement browser.runtime.onInstalled. Par conséquent, le contenu de data sera perdu lorsque le service worker sera arrêté, et toute réponse à des messages futurs échouera. Nous corrigerons cela après avoir écrit notre test.

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

Étape 2 : Installez les dépendances

Exécutez npm install pour installer les dépendances requises.

Étape 3 : Écrire un test de base

Ajoutez le test suivant en bas de index.test.js. La page de test de notre extension de test s'ouvre, l'élément de bouton est cliqué et une réponse du service worker est attendue.

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');
});

Vous pouvez exécuter votre test avec npm start et constater qu'il se termine correctement.

Étape 4 : Mettre fin au service worker

Ajoutez la fonction d'assistance suivante, qui met fin à votre 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();
}

Enfin, mettez à jour votre test avec le code suivant. Maintenant, mettez fin au service worker et cliquez à nouveau sur le bouton pour vérifier que vous avez reçu une réponse.

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');
});

Étape 5 : Exécutez votre test

Exécutez npm start. Votre test devrait échouer, ce qui indique que le service worker n'a pas répondu après avoir été arrêté.

Étape 6 : Corriger le service worker

Ensuite, corrigez le service worker en supprimant sa dépendance à l'état temporaire. Mettez à jour l'extension de test pour utiliser le code suivant, qui est stocké dans service-worker-fixed.js dans le dépôt.

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

Ici, nous enregistrons la version dans browser.storage.local au lieu d'une variable globale pour conserver l'état entre les durées de vie du service worker. Étant donné que le stockage n'est accessible qu'en mode asynchrone, nous renvoyons également la valeur "true" à partir de l'écouteur onMessage pour nous assurer que le rappel sendResponse reste actif.

Étape 7 : Exécutez à nouveau votre test

Exécutez à nouveau le test avec npm start. Il devrait maintenant réussir.

Étapes suivantes

Vous pouvez désormais appliquer la même approche à votre propre extension. Réfléchissez aux éléments suivants :

  • Créez votre suite de tests pour qu'elle puisse s'exécuter avec ou sans arrêt inattendu du service worker. Vous pouvez ensuite exécuter les deux modes individuellement pour identifier plus clairement la cause d'un échec.
  • Écrivez du code pour mettre fin au service worker à des points aléatoires d'un test. Cela peut être un bon moyen de découvrir des problèmes difficiles à prévoir.
  • Tirez des leçons des échecs de tests et essayez de coder de manière défensive à l'avenir. Par exemple, ajoutez une règle de linting pour décourager l'utilisation de variables globales et essayez de déplacer les données vers un état plus persistant.