Secure, test, and publish

Test the extension

Combine pure module tests, a repeatable manual matrix, and browser automation for the critical save-and-show journey.

You can’t unit-test chrome.tabs.query(). But most of an extension isn’t Chrome APIs. It’s logic around them, and that logic tests like any other JavaScript.

I split extension testing into three layers, by boundary.

Pure modules, tested in Node

Pull everything that doesn’t need a browser into plain modules: the URL normalization, the message validation, the state transitions of the popup. Then test them with Node’s built-in runner.

import test from 'node:test'
import assert from 'node:assert/strict'
import { keyForUrl } from '../src/keys.js'

test('drops the fragment', () => {
  assert.equal(
    keyForUrl('https://flaviocopes.com/javascript/#arrays'),
    'note:https://flaviocopes.com/javascript/'
  )
})

Run it with node --test. Fast, no browser, and this is where most regressions get caught.

Integration with fake adapters

The code that calls chrome.storage and chrome.tabs gets a thin adapter around it. In tests, swap in a fake: a storage that rejects on set(), a tabs query that returns no tab. Now you can prove the popup shows an error state on a failed write, without a browser.

Browser tests for the real thing

Some things only the packaged extension can prove: the manifest is valid, permissions are right, injection works, the worker wakes up. For those, launch Chrome with a fresh profile and load the unpacked folder:

google-chrome --user-data-dir=/tmp/page-notes-profile --load-extension=./page-notes

Playwright can do the same with a persistent context, and it can then drive the popup page and the test tab. Keep one automated flow here: open a page, save a note, reload, show the panel.

Lifecycle is the hard part

Manifest V3 bugs hide in transitions. Test them on purpose: let the worker go idle, then press the shortcut. Close the popup mid-save. Reload the extension while a tab keeps the old content script. Navigate to another origin and try to highlight. Install version 1.0.0 with saved notes and upgrade to 1.1.0.

And run the critical path with no DevTools open. An inspector keeps the worker and popup alive, which hides exactly these bugs.

Assert evidence

“No error in the console” is not a passing test. Assert what should exist: one record in storage, one panel in the DOM, a { ok: true } response, an empty Network log, the close button holding focus with an accessible name. Give each test a deterministic page, and clear storage between cases.

Build your matrix now. Key normalization, hostile text rendering, concurrent saves, close and reopen, double injection, toggle after a worker restart, reload with a stale content script, expired access, restricted pages, keyboard-only use, upgrade with existing notes. Next to each row, write which layer catches it. Anything with an empty cell is a bug you’ll find in production.

Lesson completed