Prueba la finalización de service worker con Puppeteer

En esta guía, se explica cómo compilar extensiones más sólidas probando la terminación de service worker con Puppeteer. Es importante estar preparado para manejar la finalización en cualquier momento, ya que esto puede suceder sin previo aviso, lo que puede provocar la pérdida de cualquier estado no persistente en el service worker. En consecuencia, las extensiones deben guardar un estado importante y poder controlar las solicitudes en cuanto se vuelven a iniciar cuando hay un evento que controlar.

Antes de comenzar

Clona o descarga el repositorio chrome-extensions-samples. Usaremos la extensión de prueba en /functional-samples/tutorial.terminate-sw/test-extension, que envía un mensaje al service worker cada vez que se hace clic en un botón y agrega texto a la página si se recibe una respuesta.

También deberás instalar Node.JS, que es el entorno de ejecución en el que se basa Puppeteer.

Paso 1: Inicia el proyecto de Node.js

Crea los siguientes archivos en un directorio nuevo. En conjunto, crean un proyecto de Node.js nuevo y proporcionan la estructura básica de un paquete de pruebas de Puppeteer con Jest como ejecutor de pruebas. Consulta Cómo probar extensiones de Chrome con Puppeteer para obtener información más detallada sobre esta configuración.

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

Observa que nuestra prueba carga el archivo test-extension del repositorio de muestras. El controlador de chrome.runtime.onMessage se basa en el estado establecido en el controlador del evento chrome.runtime.onInstalled. Como resultado, el contenido de data se perderá cuando se cierre el service worker y la respuesta a cualquier mensaje futuro fallará. Corregiremos esto después de escribir la prueba.

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

Paso 2: Instala las dependencias

Ejecuta npm install para instalar las dependencias requeridas.

Paso 3: Escribe una prueba básica

Agrega la siguiente prueba al final de index.test.js. Se abrirá la página de prueba desde la extensión de prueba, se hará clic en el elemento de botón y se esperará una respuesta del 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');
});

Puedes ejecutar tu prueba con npm start. Deberías ver que se completa de forma correcta.

Paso 4: Finaliza el service worker

Agrega la siguiente función auxiliar que finaliza tu 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();
}

Por último, actualiza la prueba con el siguiente código. Ahora, finaliza el service worker y haz clic en el botón nuevamente para comprobar que hayas recibido una respuesta.

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

Paso 5: Ejecuta la prueba

Ejecuta npm start. La prueba debería fallar, lo que indica que el service worker no respondió después de que se cerró.

Paso 6: Corrige el service worker

A continuación, corrige el service worker. Para ello, quita su dependencia del estado temporal. Actualiza la extensión de prueba para usar el siguiente código, que se almacena en service-worker-fixed.js en el repositorio.

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

Aquí, guardamos la versión en chrome.storage.local, en lugar de una variable global para conservar el estado entre las vidas de los service worker. Dado que solo se puede acceder al almacenamiento de forma asíncrona, también mostramos el valor true desde el objeto de escucha onMessage para garantizar que se mantenga activa la devolución de llamada sendResponse.

Paso 7: Vuelve a ejecutar la prueba

Vuelve a ejecutar la prueba con npm start. Ahora debería pasar.

Próximos pasos

Ahora puedes aplicar el mismo enfoque a tu propia extensión. Ten en cuenta lo siguiente:

  • Compila tu conjunto de pruebas para que admita la ejecución con o sin finalización inesperada de service worker. Luego, puedes ejecutar ambos modos de forma individual para aclarar qué causó una falla.
  • Escribe código para finalizar el service worker en puntos aleatorios de una prueba. Esta puede ser una buena manera de descubrir problemas que pueden ser difíciles de predecir.
  • Aprende de las pruebas fallidas y trata de codificar a la defensiva en el futuro. Por ejemplo, agrega una regla de análisis con lint para desalentar el uso de variables globales y trata de mover los datos a un estado más persistente.