chrome-extension-test-loader
Loads an unpacked Chrome / Chromium extension for testing through the `chrome://extensions` Developer-mode flow: the minimum loadable `manifest.json`, the Load-unpacked directory-not-file selection, toolbar pinning, and the reload matrix deciding what a code edit actually re-evaluates (`manifest.json`, the background service worker, and content scripts need an explicit card refresh, content scripts additionally need a host-page refresh, while popup / options / other extension HTML pages re-evaluate on next open). Also covers reading the red Errors card, where service-worker and content-script logs surface, and the `--load-extension` and `web-ext --target chromium` equivalents that move the same load into CI. Scope is getting a build directory loaded and reloaded, not the runtime behaviour asserted afterwards. Use when a freshly built extension directory has to go into Chrome for the first time, or when an edit appears to have no effect and you need to know which surface requires an explicit reload.
Install with skills.sh (any agent)
npx skills add testland/qa --skill chrome-extension-test-loaderchrome-extension-test-loader
Overview
The unpacked-extension flow is the canonical Chromium developer test loop per the Chrome Extensions "Hello World" tutorial (opens in new window): toggle Developer mode in chrome://extensions, click Load unpacked, point at the source directory containing manifest.json.
Automated launchers reproduce this same load through browser launch flags rather than the UI (see CI integration below). Knowing the manual flow directly is what lets you reason about why a launcher fails to see the extension, which surface a regression hit, and what a "service worker reload" actually re-evaluates.
Scope boundary
This skill owns installation and re-installation of the extension build output: manifest shape sufficient to load, the developer load gesture, the reload matrix, the log and error surfaces that tell you the load succeeded, and the CI equivalents of the manual gesture.
It deliberately does not cover what the extension does once it is running. Assertions about chrome.runtime / chrome.tabs messaging, storage quotas, permission prompts, or DOM effects of content scripts are a separate job with a separate failure mode: those tests fail with a live extension, whereas everything here fails before the extension is live at all. If the extension appears on chrome://extensions with no error card, this skill's job is done.
When to use
Authoring
Minimum loadable manifest
Per cr-hello (opens in new window), the minimum manifest that loads is:
{
"name": "Hello Extensions",
"description": "Base Level Extension",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "hello.html",
"default_icon": "hello_extensions.png"
}
}manifest_version: 3 is the value carried by the tutorial's minimum manifest per cr-hello (opens in new window). version is validated at load time: Chrome rejects a bad one with Required value version is missing or invalid. It must be between 1-4 dot-separated integers each between 0 and 65536. per the Debug your extension tutorial (opens in new window).
Project layout
my-extension/
manifest.json # MUST live at root
background.js # service worker (manifest.background.service_worker)
content.js # content script (manifest.content_scripts[].js)
popup/
popup.html
popup.js
options/
options.html
icons/
16.png
48.png
128.pngPer cr-hello (opens in new window): "The only prerequisite is to place the manifest.json file in the extension's root directory." The directory you select in the next step is my-extension/, not my-extension/manifest.json.
Running
Step 1 - Open chrome://extensions
Per cr-hello (opens in new window): "By design chrome:// URLs are not linkable." Three routes to the page:
| Route | Steps |
|---|---|
| Direct | New tab, type chrome://extensions, press Enter |
| Toolbar | Click the Extensions puzzle icon, then Manage Extensions |
| Menu | Chrome menu, More Tools, Extensions |
Step 2 - Toggle Developer mode on
Per cr-hello (opens in new window), click the toggle switch labelled Developer mode at the top-right of the Extensions page. Three buttons appear: Load unpacked, Pack extension, Update.
Step 3 - Load unpacked
Per cr-hello (opens in new window), click Load unpacked, then select the extension's source directory. If the manifest or the service worker is rejected, a red Errors button appears on the resulting card per cr-debug (opens in new window) (see Parsing results).
Step 4 - Pin the extension to the toolbar
Per cr-hello (opens in new window): "Pin your extension to the toolbar to quickly access your extension during development." Open the puzzle-icon Extensions menu, find the row, click the pin icon. The popup then becomes a single-click target instead of a two-click one.
Step 5 - Reload after edits
The reload requirements per cr-hello (opens in new window):
| Component edited | Reload action required |
|---|---|
manifest.json | Click refresh on the extension card |
Service worker (background.service_worker) | Click refresh on the extension card |
| Content scripts | Click refresh on the extension card plus refresh the host page |
| Popup HTML / JS | None, next open re-evaluates |
| Options page | None, next open re-evaluates |
| Other extension HTML pages | None |
The "click refresh on the card" gesture is what an automated harness reproduces either by toggling chrome.management.setEnabled(id, false) then setEnabled(id, true) (which requires the "management" permission per the chrome.management reference (opens in new window)), or by closing and re-launching the persistent browser context.
Worked example: confirming a fresh build loads
1. npm run build -> produces dist/
2. chrome://extensions -> Developer mode ON
3. Load unpacked -> select dist/ (the folder, not the file)
4. Expect: a card titled with manifest.name, an ID string,
a "Service worker" link, and NO red "Errors" button.
5. Edit src/content.js, rebuild.
6. Click the card's refresh icon, then reload the host tab.
7. Re-check the card: still no Errors button.Expected output shape of a healthy card:
[icon] Hello Extensions 1.0 [toggle ON]
Base Level Extension
ID: abcdefghijklmnopabcdefghijklmnop
Inspect views: service worker
[Details] [Remove] [Refresh]A failed load replaces that with a red Errors button.
Parsing results
Manifest error cards
Per cr-debug (opens in new window), when the extension fails to load or register, an Errors button appears in red on the extension's card on chrome://extensions. Clicking it lists the messages. Two exact strings worth recognising, both quoted in cr-debug (opens in new window):
Service-worker logs
Per cr-debug (opens in new window), click the blue Inspect views link next to the extension to open DevTools scoped to the service worker. console output, the network panel, and breakpoints all work there.
Content-script logs
Per cr-debug (opens in new window), content scripts run inside the web page, so their errors surface in the host page's DevTools, not the extension's. Use the context dropdown next to top in the console to switch from the page's context to the extension's.
Popup logs
Per cr-debug (opens in new window), popup errors also appear behind the Errors button, and the popup itself can be inspected by right-clicking it and choosing inspect, which opens a DevTools window for that popup instance.
CI integration
The manual chrome://extensions gesture is not automatable (see Limitations). Two supported ways to perform the same load unattended:
1. Chrome launch flags on a persistent context. Per the Playwright Chrome extensions guide (opens in new window):
const browserContext = await chromium.launchPersistentContext(userDataDir, {
channel: 'chromium',
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`
]
});pw-ext (opens in new window) states channel: 'chromium' is what "allows to run extensions in headless mode", because Chrome and Edge removed the command-line flags needed for side-loading.
2. web-ext against a Chromium binary. Per the web-ext command reference (opens in new window), --target chromium runs the extension in a Chromium-based browser, and --chromium-binary takes a "Path or alias to a Chromium executable such as google-chrome, google-chrome.exe, or opera.exe. If not specified, the default Google Chrome is used."
web-ext run --target chromium --source-dir ./distA minimal CI smoke job launches the context, waits for the extension's service worker to appear, and exits non-zero if it never does. That gates the slower behavioural jobs on "the build directory is loadable" rather than letting a manifest typo present itself as fifty failing tests.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Selecting manifest.json itself in Load unpacked | Chrome expects the extension's root directory per cr-hello (opens in new window) | Select the parent folder of manifest.json |
| Editing a content script and expecting the next page load to pick it up | Content scripts need a card refresh and a host-page refresh per cr-hello (opens in new window) | Refresh both |
| Reloading the card after a popup-only edit | Popup re-evaluates on next open per cr-hello (opens in new window); the reload is wasted time and resets service-worker state | Just reopen the popup |
| Assuming pinned-toolbar state survives a fresh profile | Pinning is per-profile UI state, set through the Extensions menu per cr-hello (opens in new window) | Re-pin after every fresh-profile launch, or drive the popup through the puzzle menu |
Loading via channel: 'chrome' in headless CI | Chrome and Edge removed the side-loading flags; only channel: 'chromium' runs extensions headless per pw-ext (opens in new window) | Set channel: 'chromium' |
| Leaving service-worker DevTools open while testing idle behaviour | Per cr-debug (opens in new window), an open DevTools window keeps the service worker active | Close DevTools before exercising termination |
Treating Service worker registration failed. Status code: 15. as the root cause | It is a wrapper; the real error is printed under it per cr-debug (opens in new window) | Read the following line |
Limitations
References
Related skills
chrome-extension-messaging-tests
Asserts Chrome extension message-passing behaviour against a running extension: one-shot `chrome.runtime.sendMessage` plus the literal `return true` that holds the response channel open for an async `sendResponse`, `chrome.tabs.sendMessage` into one tab's content script, long-lived `chrome.runtime.connect` ports and their `onDisconnect` triggers, web-page messages gated by `externally_connectable`, and `chrome.runtime.connectNative` native-messaging hosts. Covers the payload rules a test must respect (Chrome uses JSON serialization rather than structured clone, so `Map` / `Set` / `Date` do not round-trip; maximum message size is 64 MiB) and the first-listener-wins rule when several `onMessage` listeners are registered. Scope is messaging behaviour on an already-running extension, not the install or reload step. Use when a message reaches no listener, a `sendResponse` callback never fires, a port disconnects mid-test, or a page origin has to be proven allowed before publishing.
extension-storage-test-author
Build-an-X workflow that emits a `chrome.storage` test suite. Picks the right area (`storage.local` 10 MB / `storage.sync` 100 KB total + 8 KB per item + 512 items + 1,800 writes/hour / `storage.session` 10 MB in-memory MV3-only / `storage.managed` read-only enterprise-policy) per access pattern, then generates tests for quota-exceeded behavior (`runtime.lastError` callback path + rejected promise async path), `storage.sync` per-item + total quotas, `storage.onChanged` event payload shape, multi-area write isolation, and Firefox-Chrome divergences (Firefox `storage.sync` quotas align with Chrome per MDN; Firefox `storage.managed` available; Firefox `storage.session` MV3-only). Output: a per-extension storage test file + matrix asserting the right area was chosen. Use when an extension persists state across sessions or devices and no test proves the chosen storage area survives its quota limits.
manifest-v3-test-surface-reference
Pure-reference catalog of the Manifest V3 test surface for Firefox + Chromium browser extensions. Maps each manifest field that changed from MV2 (manifest_version, background.service_worker vs background.scripts, action vs browser_action / page_action, host_permissions split, web_accessible_resources object-form, content_security_policy object-form), the runtime restrictions service workers impose (no DOM, no XMLHttpRequest, no localStorage, ephemeral lifecycle, synchronous listener registration, alarms instead of setTimeout), and the Firefox-vs-Chrome key matrix (browser_specific_settings.gecko, externally_connectable / offline_enabled gaps, MV2-only user_scripts manifest key). Use as the manifest-surface reference when authoring extension tests across both browsers.
mv2-to-mv3-migration-test-checklist
Build-an-X workflow that emits a per-extension MV2 → MV3 migration test checklist. Walks the six canonical migration sections (manifest, service worker, API calls, declarative net request, security, publication) per the Chrome migration checklist, then for each one inventories the source MV2 manifest, names the MV3 replacement field / API, and emits the verification test cases. Covers the Firefox-Chrome divergence cells (page_action retained in Firefox, event pages allowed in Firefox 106+, host-permission install-prompt behavior changed in Firefox 127, web_accessible_resources `use_dynamic_url` Chromium-only). Output: a checklist artifact with per-section test cases the migrating extension must pass before publishing the MV3 build. Use when migrating an MV2 extension to Manifest V3 and the team needs section-by-section evidence the migration is complete.
playwright-extension-fixtures
Author the lower-level Playwright fixture pattern that every Chromium extension test depends on - `chromium.launchPersistentContext` with `--disable-extensions-except=$DIR` + `--load-extension=$DIR`, the `channel: 'chromium'` selection that unlocks headless extension support, the `context.serviceWorkers()` + `waitForEvent('serviceworker')` race-handling pattern, and the `extensionId = serviceWorker.url().split('/')[2]` extraction recipe. This is the launch-and-load layer shared by every extension test, not the assertions run on top of it. Use when authoring or debugging the fixture a Chromium extension test imports - popup, content script, service worker, options page, or side panel.
web-ext-cli-mozilla
Author, lint, run, build, and sign a Firefox / Chromium WebExtension using Mozilla's `web-ext` CLI v8. Covers `web-ext lint` (addons-linter wrapper, JSON output for CI), `web-ext run` (temporary install in firefox-desktop / firefox-android / chromium targets with hot-reload), `web-ext build` (deterministic zip), and `web-ext sign` (AMO submission API, listed vs unlisted channels, JWT credentials). Use when the extension targets Firefox (signing is mandatory for distribution) or when cross-browser test runs need a single CLI that drives both Firefox and Chromium against the same source tree.