> [!NOTE]
> **Note:** For general advice on testing extensions, see [End-to-end testing of Chrome Extensions](https://developer.chrome.com/docs/extensions/mv3/end-to-end-testing) and [Unit testing in Chrome Extensions](https://developer.chrome.com/docs/extensions/mv3/unit-testing).

This guide explains how to build more robust extensions by testing service
worker termination using Puppeteer. Being prepared to handle termination at any
time is important because this can happen without warning resulting in any non-persistent state in the
service worker being lost. Consequently, extensions must save important state and
be able to handle requests as soon as they are started up again when there is an
event to handle.

## Before you start

Clone or download the [chrome-extensions-samples](https://github.com/GoogleChrome/chrome-extensions-samples) repository.
We'll use the test extension in
[`/functional-samples/tutorial.terminate-sw/test-extension`](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/tutorial.terminate-sw/test-extension) which sends a message
to the service worker each time a button is clicked and adds text to the page
if a response is received.

You'll also need to install [Node.JS](https://nodejs.org/) which is the runtime that Puppeteer is built on.

## Step 1: Start your Node.js project

Create the following files in a new directory. Together they create a new
Node.js project and provide the basic structure of a Puppeteer test suite
using [Jest](https://jestjs.io/) as a test runner. See
[Test Chrome Extensions with Puppeteer](https://developer.chrome.com/docs/extensions/how-to/test/puppeteer.md.txt) to learn about this setup
in more detail.

> [!IMPORTANT]
> **Important:** This tutorial uses the WebWorker.close() method which was added in Puppeteer v22.1.0.

package.json:

```javascript
{
  "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:

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

Notice that our test loads the `test-extension` from the samples repository.
The handler for `browser.runtime.onMessage` relies on state set in the handler
for the `browser.runtime.onInstalled` event. As a result, the contents of `data`
will be lost when the service worker is terminated and responding to any future
messages will fail. We will fix this after writing our test.

service-worker-broken.js:

```javascript
let data;

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

browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  sendResponse(data.version);
});
```

## Step 2: Install dependencies

Run `npm install` to install the required dependencies.

## Step 3: Write a basic test

Add the following test to the bottom of `index.test.js`. This opens the test
page from our test extension, clicks the button element and waits for a response
from the service worker.

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

You can run your test with `npm start` and should see that it completes
successfully.

## Step 4: Terminate the service worker

Add the following helper function which terminates your service worker:

```javascript
/**
 * 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();
}
```

Finally, update your test with the following code. Now terminate the service
worker and click the button again to check that you received a response.

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

## Step 5: Run your test

Run `npm start`. Your test should fail which indicates that the service worker
did not respond after it was terminated.

## Step 6: Fix the service worker

Next, fix the service worker by removing its reliance on temporary state. Update
the test-extension to use the following code, which is stored in
`service-worker-fixed.js` in the repository.

service-worker-fixed.js:

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

Here, we save the version to `browser.storage.local` instead of a global variable
to persist the state between service worker lifetimes. Since storage can only
be accessed asynchronously, we also return true from the `onMessage` listener to
make sure the `sendResponse` callback stays alive.

## Step 7: Run your test again

Run the test again with `npm start`. It should now pass.

## Next steps

You can now apply the same approach to your own extension. Consider the
following:

- Build your test suite to support running with or without unexpected service worker termination. You can then run both modes individually to make it clearer what caused a failure.
- Write code to terminate the service worker at random points within a test. This can be a good way to discover issues that may be hard to predict.
- Learn from test failures and try to code defensively in the future. For example, add a linting rule to discourage the use of global variables and try to move data into more persistent state.