# Write Tests for a Chrome Extension

Yes. An agent writes Jest tests for your extension's popup, options page and service worker with the chrome APIs mocked, runs npm test and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/write-extension-tests/chrome-extension/), in a Project that has your repository.

## What the agent does

The agent clones the extension's repository into its own cloud sandbox and maps what each part does with the `chrome.*` APIs. In a Manifest V3 starter like chibat/chrome-extension-typescript-starter, `public/manifest.json` declares a popup, an options page, a content script on `<all_urls>`, a service worker at `js/background.js`, and only the `storage` permission. `src/popup.tsx` reads the active tab with `chrome.tabs.query`, sets the badge with `chrome.action.setBadgeText` every time its counter changes, and sends `{ color: "#555555" }` to the tab with `chrome.tabs.sendMessage`. `src/options.tsx` loads `favoriteColor` (default `red`) and `likesColor` (default `true`) with `chrome.storage.sync.get` and saves them with `chrome.storage.sync.set`. `src/content_script.tsx` listens on `chrome.runtime.onMessage` and changes the page background, and `src/background.ts` re-arms a 30-second `setTimeout`.

Then it finds what stops these files from being tested today. `jest.config.js` only transforms `\.ts$` files with `ts-jest`, so `popup.tsx`, `options.tsx` and `content_script.tsx` cannot be loaded, and there is no `jsdom` environment for React. The only test, `src/__tests__/sum.ts`, checks `sum(1, 1)`. Both pages also call `createRoot(document.getElementById("root")!)` when the module is imported, so a test that imports them crashes on a missing element. The agent extends the Jest transform to `.tsx`, adds `jest-environment-jsdom` and `@testing-library/react`, and either exports the `Popup` and `Options` components so tests can render them, or creates the `#root` element before importing; it lists that change in the pull request.

It mocks the Chrome APIs with `jest-chrome`, which provides typed `jest.fn()` stand-ins for the `chrome` global, and checks the installed Jest version against its peer range first, since the project is on Jest 29. The popup tests render the component, resolve `tabs.query` with a fake tab and check the URL appears, click count up and check `setBadgeText` was called with `"1"`, and click change background and check `sendMessage` got the tab id and the colour. The options tests check the defaults load, a change is saved with both keys, and the `Options saved.` status appears and clears after a second, using fake timers. The content script tests dispatch messages with and without a `color` and check the background and the `sendResponse` text, and the service worker test advances fake timers by 30 seconds and checks the poll re-arms.

Last, it runs `npm test` until the suite passes and `npm run build` to confirm the webpack bundle in `webpack/webpack.prod.js` still builds with the changes, the same two commands the repository's `build.yml` workflow runs on Node 20 and 22.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Write Jest tests for this Chrome extension's popup, options page, content script and background service worker. Read the manifest and each source file first, and list every chrome.* call and what the UI does with its result. Use jest-chrome for the chrome APIs; check its peer dependency range against the installed Jest first and report any mismatch. Make the Jest config handle .tsx files and add a jsdom environment and React Testing Library if they are missing. If a page renders itself at import time, make the smallest change that lets a test render the component, and list it. Cover storage defaults and saves, tab queries, messages with and without the expected fields, badge updates and timers, using fake timers instead of waiting. Run npm test and npm run build until both pass, and open a pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch. For an extension shaped like this starter, the diff adds test files under `src/__tests__/` for the popup, options page, content script and service worker, a Jest setup file that installs `jest-chrome`, the `jest.config.js` change for `.tsx` and `jsdom`, the new dev dependencies in `package.json` and `package-lock.json`, and the small export change that lets `Popup` and `Options` render in a test. The description lists each `chrome.*` call and the test that covers it.

The session transcript keeps every command with its output: `npm ci`, each `npm test` run with its failures and the final pass, and `npm run build`. The pull request's checks run the repository's `build.yml` on its Node matrix, which installs, builds and runs `npm test`.

Before merging, a reviewer checks the findings the tests surfaced, such as a popup that reads `tab.url` while the manifest grants only `storage` and relies on `host_permissions` for URL access, and checks that the export change did not alter what the built `popup.js` and `options.js` render.

## Run it on every Chrome release

Chrome changes extension APIs on its own schedule, so a run per Chrome release is a cheap check: a scheduled job on your side sends `POST /o/{org}/sessions` with the repository in `resources` and a prompt to update the tests and the `@types/chrome` version, run them, and open a pull request only if something changed.

Set a `webhook_url` on the API key and Rush posts `run.completed` or `run.failed` to it when the turn ends, with a `session_url` that reads the result and any pull request.

## Why run it on Rush?

Getting an extension testable means changing the Jest config, adding a DOM environment and resolving peer dependency ranges, which is exactly the kind of install that leaves a laptop's `node_modules` in a state nobody can reproduce. In a Rush sandbox the agent runs `npm ci` on a clean machine, adds what is needed, and the lockfile in the pull request is the one that produced the passing run.

Every command is kept with its output, so the reviewer can see each Jest failure that led to a config change and the final passing run, and the pull request's checks repeat it on the repository's own Node matrix.

## Frequently asked questions

### Do the tests run in a real Chrome?

No. These are unit tests in Jest with `jsdom` and a mocked `chrome` global, which run in seconds and catch logic errors in how the extension calls the APIs. For loading the unpacked extension in a real browser, ask for Playwright tests in `input` as a separate job.

### Why jest-chrome instead of hand-written mocks?

It mirrors the `chrome` namespace with typed `jest.fn()` functions and event objects that support `callListeners`, so a test can fire `runtime.onMessage` the way Chrome would. If its peer range does not accept your Jest version, the agent reports it and asks before downgrading anything.

### Will it change my extension's source code?

Only where a file cannot be imported in a test, such as a page that renders itself at import time. The agent makes the smallest change, usually exporting the component, and lists every non-test change in the pull request.

### Does it work with Vite or Plasmo extensions?

Yes. The agent reads whichever bundler and test runner the repository uses. In a Vite project it would use Vitest with the same `chrome` mocks instead of adding Jest.
