git:20260724.d4db756 to git:20260728.1bce424
85 added, 246 removed. Audit B to B.
---
name: manifest-v3-test-surface-reference
description: "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."
metadata:
keywords: "browser-extension, manifest-v3, mv3, firefox-webextensions, chrome-extensions"
---
# manifest-v3-test-surface-reference
## Overview
- Manifest V3 (MV3) is the current packaging contract for Chromium-
- family browser extensions; Firefox supports it as a peer with a
- documented divergence list. The manifest is the *only* declarative
- input both browsers see - every test surface (background lifecycle,
- permission prompt, content-script injection, web-accessible-resource
- fetch) hangs off a manifest field. Knowing which field maps to which
- runtime behaviour is what lets a test author decide whether a
+ Manifest V3 (MV3) is the current packaging contract for Chromium-family browser
+ extensions; Firefox supports it as a peer with a documented divergence list. The
+ manifest is the *only* declarative input both browsers see - every test surface
+ (background lifecycle, permission prompt, content-script injection,
+ web-accessible-resource fetch) hangs off a manifest field. Knowing which field
+ maps to which runtime behaviour is what lets a test author decide whether a
behaviour is testable in unit, integration, or full-browser scope.
- This skill is the **pure reference** consumed by the per-tool
- wrappers (`web-ext-cli-mozilla`,
- `chrome-extension-test-loader`, `playwright-extension-fixtures`) and
- the two builders (`mv2-to-mv3-migration-test-checklist`,
- `extension-storage-test-author`).
+ This skill is the **pure reference** consumed by the per-tool wrappers
+ (`web-ext-cli-mozilla`, `chrome-extension-test-loader`,
+ `playwright-extension-fixtures`) and the two builders
+ (`mv2-to-mv3-migration-test-checklist`, `extension-storage-test-author`). The
+ summary tables below are the quick lookup; exact MV2/MV3 field shapes, the full
+ Firefox/Chrome key matrix, and the offscreen-document + keep-alive detail live in
+ [references/manifest-matrix.md](references/manifest-matrix.md). All sources are
+ consolidated in the References section.
For Playwright-driven MV3 popup / content-script fixtures see
- `browser-extension-tests`.
- That skill is a Chromium-only, popup + content-script + service-
- worker-fixture skill. This reference is browser-agnostic, manifest-
- field-keyed, and covers the Firefox column explicitly.
+ `browser-extension-tests` - a Chromium-only popup + content-script +
+ service-worker-fixture skill. This reference is browser-agnostic,
+ manifest-field-keyed, and covers the Firefox column explicitly.
## When to use
- - Authoring a new MV3 extension test - which manifest fields exist,
- which produce permission prompts, which are Firefox-only.
- - Migrating an existing MV2 test suite - the field-for-field rename
- table.
- - Triaging a behaviour gap between Chrome and Firefox - the
- divergence matrix.
- - Reviewing a PR that adds a new manifest field - does it need a
- Firefox counterpart, a host-permission consequence, or a CSP
- adjustment.
-
- ## The manifest fields that change between MV2 and MV3
+ - Authoring a new MV3 extension test - which manifest fields exist, which
+ produce permission prompts, which are Firefox-only.
+ - Migrating an existing MV2 test suite - the field-for-field rename table.
+ - Triaging a behaviour gap between Chrome and Firefox - the divergence matrix.
+ - Reviewing a PR that adds a new manifest field - does it need a Firefox
+ counterpart, a host-permission consequence, or a CSP adjustment.
- Per [Update the manifest][cr-mig-manifest] and
- [Manifest V3 migration overview][cr-mig-overview]:
+ ## Manifest fields that change between MV2 and MV3
- [cr-mig-manifest]: https://developer.chrome.com/docs/extensions/develop/migrate/manifest
- [cr-mig-overview]: https://developer.chrome.com/docs/extensions/develop/migrate
+ Summary lookup; the exact MV2/MV3 shapes for `background`, `host_permissions`,
+ and `web_accessible_resources` are in
+ [references/manifest-matrix.md](references/manifest-matrix.md).
| Field | MV2 | MV3 | Test implication |
|---|---|---|---|
| `manifest_version` | `2` | `3` | First assertion in any conformance test |
| `background` | `{ "scripts": [...], "persistent": false }` | `{ "service_worker": "sw.js", "type": "module"? }` | Lifecycle test moves from "always running" to "wake on event, terminate idle" |
| `action` / `browser_action` / `page_action` | `browser_action` or `page_action` | `action` (unified) | Popup-rendering tests target the unified `action` slot |
| `permissions` | API + host strings mixed | API strings only | API-permission tests stay; host-permission tests move (see next row) |
| `host_permissions` | (did not exist) | match patterns moved here from `permissions` | Each host pattern is a runtime permission prompt - testable as a user gesture flow |
| `optional_host_permissions` | (did not exist) | runtime-requestable hosts | Tests must drive `permissions.request` from a user gesture |
| `web_accessible_resources` | flat string array | array of `{ resources: [...], matches: [...] }` objects | Cross-origin `fetch` of an extension resource is only allowed from a matching `matches` pattern |
| `content_security_policy` | string | object with `extension_pages` / `sandbox` keys | No inline `<script>`, no remotely hosted code, no `eval` - testable via load-time CSP violations |
- ### `background` field - exact shape
-
- MV2 (per [Update the manifest][cr-mig-manifest] and
- [Migrate to a service worker][cr-mig-sw]):
-
- [cr-mig-sw]: https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers
-
- ```json
- {
- "background": {
- "scripts": ["backgroundContextMenus.js", "backgroundOauth.js"],
- "persistent": false
- }
- }
- ```
-
- MV3 (per [Migrate to a service worker][cr-mig-sw]):
-
- ```json
- {
- "background": {
- "service_worker": "service_worker.js",
- "type": "module"
- }
- }
- ```
-
- The MV3 `service_worker` field is a *single string* (not an array);
- `type` is optional and only valid as `"module"`. The MV2 `persistent`
- flag is removed entirely.
-
- ### `host_permissions` split
-
- Per [Update the manifest][cr-mig-manifest]:
-
- > "Host permissions in Manifest V3 are a separate field; you don't
- > specify them in `"permissions"` or in `"optional_permissions"`."
-
- MV2:
-
- ```json
- "permissions": ["tabs", "bookmarks", "https://www.blogger.com/"],
- "optional_permissions": ["unlimitedStorage", "*://*/*"]
- ```
-
- MV3:
-
- ```json
- "permissions": ["tabs", "bookmarks"],
- "optional_permissions": ["unlimitedStorage"],
- "host_permissions": ["https://www.blogger.com/"],
- "optional_host_permissions": ["*://*/*"]
- ```
-
- `"content_scripts[].matches"` is **unchanged** between MV2 and MV3.
-
- ### `web_accessible_resources` shape change
-
- Per [Update the manifest][cr-mig-manifest]:
-
- MV2 (flat string array):
-
- ```json
- "web_accessible_resources": [
- "images/*",
- "style/extension.css",
- "script/extension.js"
- ]
- ```
-
- MV3 (array of objects, each scoping resources to URL patterns or
- extension IDs):
-
- ```json
- "web_accessible_resources": [
- { "resources": ["images/*"], "matches": ["*://*/*"] },
- {
- "resources": ["style/extension.css", "script/extension.js"],
- "matches": ["https://example.com/*"]
- }
- ]
- ```
-
- Test implication: a page-context `fetch(chrome.runtime.getURL('...'))`
- that worked under MV2 may 404 under MV3 if the requester's origin
- isn't covered by a `matches` pattern.
-
## Service-worker runtime restrictions (MV3-only test surface)
- Per [Migrate to a service worker][cr-mig-sw], the background context
- in MV3 is a service worker - not a persistent page - and inherits
- the standard service-worker constraints plus a few extension-specific
- ones:
+ The MV3 background context is a service worker - not a persistent page - and
+ inherits the standard service-worker constraints plus a few extension-specific
+ ones. The offscreen-document construct and the keep-alive heartbeat are detailed
+ in [references/manifest-matrix.md](references/manifest-matrix.md).
| Constraint | MV2 | MV3 | Test implication |
|---|---|---|---|
| DOM / `window` | available | unavailable | Anything touching DOM moves to an offscreen document (`chrome.offscreen.createDocument`) |
| `XMLHttpRequest` | available | unavailable - use `fetch()` | XHR-using test fixtures must be rewritten |
| `localStorage` | available | unavailable - use `chrome.storage.local` | Tests asserting persisted state must use `chrome.storage.*` (see `extension-storage-test-author`) |
| `setTimeout` / `setInterval` | reliable | cancelled when worker terminates - use `chrome.alarms` | Tests timing background work must use alarms, not timers |
| Listener registration | top-level or async | **must be synchronous at top level** | Async-registered listeners are "not guaranteed to work in Manifest V3" |
- | Lifecycle | persistent | ephemeral (start โ run โ terminate, repeated) | Globals reset; storage is source of truth |
-
- Quote from [Migrate to a service worker][cr-mig-sw]:
-
- > "Registering a listener asynchronously (for example inside a
- > promise or callback) is not guaranteed to work in Manifest V3."
-
- > "[Service workers] are ephemeral, which means they'll likely
- > start, run, and terminate repeatedly."
-
- The keep-alive heartbeat pattern (calling `chrome.runtime.getPlatformInfo`
- on a ~25s interval to reset the idle timer, or writing to
- `chrome.storage.local` every 20s) is documented but
- [Chrome explicitly limits its use][cr-mig-sw] to enterprise/education
- managed extensions, "reserves the right to take action" against
- others, and notes a `waitUntil()`-style API is under discussion in
- the W3C WebExtensions Community Group (WECG).
-
- ### Offscreen documents
-
- Per [Migrate to a service worker][cr-mig-sw], DOM-requiring work in
- MV3 goes to an offscreen document:
-
- ```js
- chrome.offscreen.createDocument({
- url: chrome.runtime.getURL('offscreen.html'),
- reasons: ['CLIPBOARD'],
- justification: 'testing the offscreen API',
- });
- ```
+ | Lifecycle | persistent | ephemeral (start -> run -> terminate, repeated) | Globals reset; storage is source of truth |
- Offscreen documents communicate with the service worker via
- `runtime.sendMessage` / `runtime.onMessage` only - they don't share
- other extension APIs.
+ Two load-bearing quotes from [cr-mig-sw]: "Registering a listener
+ asynchronously (for example inside a promise or callback) is not guaranteed to
+ work in Manifest V3," and "[Service workers] are ephemeral, which means they'll
+ likely start, run, and terminate repeatedly."
## Firefox vs Chrome manifest key matrix
- Per [MDN manifest.json keys reference][mdn-manifest], the table
- below enumerates every documented manifest key with its
- Firefox / Chrome availability and MV2 / MV3 status. Tests must
- either gate on browser detection or split into per-browser fixtures
- when the key appears as "not supported" in one column.
-
- [mdn-manifest]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json
-
- | Key | MV2 | MV3 | Firefox | Chrome | Test note |
- |---|---|---|---|---|---|
- | `manifest_version` | yes | yes | yes | yes | Mandatory; first assertion |
- | `name` | yes | yes | yes | yes | Mandatory |
- | `version` | yes | yes | yes | yes | Mandatory; semver in Firefox AMO |
- | `action` | no | yes | yes | yes | Unified popup slot |
- | `browser_action` | yes | no | yes (MV2 only) | yes (MV2 only) | Renames to `action` in MV3 |
- | `page_action` | yes | no | yes (MV2; different in MV3) | yes (MV2 only) | Firefox keeps a different page-action shape |
- | `background` | yes | yes | yes | yes | Shape changes per above |
- | `browser_specific_settings` | yes | yes | yes | **no** | Tests asserting extension ID stability rely on this in Firefox |
- | `content_scripts` | yes | yes | yes | yes | Same shape |
- | `content_security_policy` | yes | yes | yes | yes | Shape changes (object in MV3) |
- | `declarative_net_request` | yes | yes | yes | yes | Replaces `webRequest`-blocking surface |
- | `externally_connectable` | yes | yes | **no** | yes | Cross-origin runtime messaging - Chrome only |
- | `host_permissions` | no | yes | yes | yes | New in MV3; permission-prompt test surface |
- | `offline_enabled` | yes | yes | **no** | yes | Chrome-only manifest key |
- | `optional_host_permissions` | no | yes | yes | yes | Runtime host grants |
- | `optional_permissions` | yes | yes | yes | yes | Runtime API grants |
- | `permissions` | yes | yes | yes | yes | API permissions only in MV3 |
- | `protocol_handlers` | yes | yes | **yes (Firefox only)** | no | Firefox-only register-protocol surface |
- | `sidebar_action` | yes | yes | yes (Firefox/Opera) | no | Sidebar UI - not in Chrome |
- | `storage` (as manifest key) | yes | yes | **no** | yes | Note: the `storage` *API* works in both |
- | `theme_experiment` | yes | yes | **yes (Firefox only)** | no | Experimental theming |
- | `user_scripts` (manifest key) | yes | no | yes (MV2 only) | yes (MV2 only) | MV3 replaces with `userScripts` API |
- | `web_accessible_resources` | yes | yes | yes | yes | Shape changes (object array in MV3) |
-
- ### `browser_specific_settings.gecko`
-
- Per [MDN manifest.json keys reference][mdn-manifest], Firefox-
- specific metadata (extension ID, min Firefox version) lives under
- `browser_specific_settings.gecko`. Chrome silently ignores this key:
-
- ```json
- {
- "browser_specific_settings": {
- "gecko": {
- "id": "@addon-example",
- "strict_min_version": "42.0"
- }
- }
- }
- ```
+ The full 23-key availability matrix (MV2 / MV3 x Firefox / Chrome) and the
+ `browser_specific_settings.gecko` example are in
+ [references/manifest-matrix.md](references/manifest-matrix.md). The divergences
+ that most often break a cross-browser test:
- Test note: AMO ([addons.mozilla.org]) submission validation requires
- a stable `gecko.id` for signing - a test asserting that the built
- zip carries a deterministic ID prevents accidental ID drift across
- builds.
+ - `externally_connectable`, `offline_enabled`, `storage` (manifest key) - Chrome
+ only; gate Chrome assertions on browser detection.
+ - `browser_specific_settings` - Firefox requires it (stable `gecko.id` for AMO
+ signing); Chrome silently ignores it.
+ - `protocol_handlers`, `sidebar_action`, `theme_experiment` - Firefox only.
+ - `user_scripts` (manifest key) - MV2 only; MV3 replaces it with the
+ `userScripts` API.
+ - `page_action` - Firefox keeps a different page-action shape in MV3; Chrome
+ drops it for the unified `action`.
## MV3 platform availability
- Per [Manifest V3 migration overview][cr-mig-overview]: *"Manifest V3
- is supported generally in Chrome 88 or later"*, with some
- replacement APIs landing after 88. `minimum_chrome_version` in the
- manifest pins a floor for users on stable channels.
+ Manifest V3 is supported generally in Chrome 88 or later, with some replacement
+ APIs landing after 88. `minimum_chrome_version` in the manifest pins a floor for
+ users on stable channels.
- The MV2 deprecation timeline itself lives on a separate Chrome
- "Manifest V2 support timeline" page - cite by stable URL
+ The MV2 deprecation timeline lives on a separate Chrome "Manifest V2 support
+ timeline" page - cite by stable URL
(`developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline`)
and read live, as dates have shifted multiple times.
## Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
- | Assuming MV3 `permissions` still accepts host match patterns | Lint passes; runtime drops them silently | Move all host patterns to `host_permissions` per [cr-mig-manifest] |
- | Testing `background.scripts` array under MV3 | Field doesn't exist in MV3; manifest fails to load | Use `background.service_worker` single string per [cr-mig-sw] |
+ | Assuming MV3 `permissions` still accepts host match patterns | Lint passes; runtime drops them silently | Move all host patterns to `host_permissions` |
+ | Testing `background.scripts` array under MV3 | Field doesn't exist in MV3; manifest fails to load | Use `background.service_worker` single string |
| Using `localStorage` in service-worker tests | Throws in MV3 | Use `chrome.storage.local` (see `extension-storage-test-author`) |
- | Registering `chrome.runtime.onMessage` inside a promise | Listener may not fire after worker restart in MV3 | Register synchronously at top level per [cr-mig-sw] |
- | Testing flat-string `web_accessible_resources` under MV3 | Resources unreachable from page context | Use object form `{ resources, matches }` per [cr-mig-manifest] |
+ | Registering `chrome.runtime.onMessage` inside a promise | Listener may not fire after worker restart in MV3 | Register synchronously at top level |
+ | Testing flat-string `web_accessible_resources` under MV3 | Resources unreachable from page context | Use object form `{ resources, matches }` |
| Treating Firefox MV3 as Chrome MV3 | `externally_connectable`, `offline_enabled` not supported; `browser_specific_settings` required | Run Firefox tests with `web-ext` (see `web-ext-cli-mozilla`) and gate Chrome-only assertions |
- | Using `setTimeout` to delay background work | Cancelled on worker termination | Use `chrome.alarms.create` per [cr-mig-sw] |
+ | Using `setTimeout` to delay background work | Cancelled on worker termination | Use `chrome.alarms.create` |
| Polling via 1s heartbeat to keep SW alive | Chrome explicitly limits keepalive abuse | Restructure to event-driven; offscreen document for long DOM work |
## Limitations
- - **Firefox MV3 parity is incomplete.** Several MV3 APIs landed on
- Firefox after Chrome; check [MDN's per-API browser-compat
- tables][mdn-manifest] before asserting cross-browser behaviour.
- - **`declarativeNetRequest` rule shape detail** lives on a separate
- Chrome migration page (not extracted here) - fetch
+ - **Firefox MV3 parity is incomplete.** Several MV3 APIs landed on Firefox after
+ Chrome; check [MDN's per-API browser-compat tables][mdn-manifest] before
+ asserting cross-browser behaviour.
+ - **`declarativeNetRequest` rule shape detail** lives on a separate Chrome
+ migration page (not extracted here) - fetch
`developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest`
before authoring rule-equivalence tests.
- - **No coverage of `content_security_policy.extension_pages`
- exact-string syntax.** The MV3 `content_security_policy` object
- shape is documented separately at
+ - **No coverage of `content_security_policy.extension_pages` exact-string
+ syntax.** The MV3 `content_security_policy` object shape is documented
+ separately at
`developer.chrome.com/docs/extensions/reference/manifest/content-security-policy`.
- - **Edge / Opera / Brave coverage.** Chromium-fork browsers track
- Chrome's MV3 timeline but may apply different store-side policies
- (Edge add-ons store, Opera add-ons). Cite the upstream Chrome
- source and add per-store rerun if needed.
+ - **Edge / Opera / Brave coverage.** Chromium-fork browsers track Chrome's MV3
+ timeline but may apply different store-side policies (Edge add-ons store, Opera
+ add-ons). Cite the upstream Chrome source and add per-store rerun if needed.
## References
- - Chrome MV3 migration overview -
- [developer.chrome.com/docs/extensions/develop/migrate][cr-mig-overview].
- - Chrome - Update the manifest (MV2 โ MV3 manifest field rename
- list) - [developer.chrome.com/docs/extensions/develop/migrate/manifest][cr-mig-manifest].
- - Chrome - Migrate to a service worker (background field, SW
- lifecycle, offscreen docs, keepalive) -
- [developer.chrome.com/docs/extensions/develop/migrate/to-service-workers][cr-mig-sw].
- - MDN WebExtensions manifest.json keys reference (Firefox key
- matrix, `browser_specific_settings`) - [mdn-manifest].
- - Consumed by:
- `web-ext-cli-mozilla`,
- `chrome-extension-test-loader`,
- `playwright-extension-fixtures`,
- `mv2-to-mv3-migration-test-checklist`,
+ - Chrome - Manifest V3 migration overview - [cr-mig-overview].
+ - Chrome - Update the manifest (MV2 -> MV3 field rename list) - [cr-mig-manifest].
+ - Chrome - Migrate to a service worker (background field, SW lifecycle, offscreen
+ docs, keepalive) - [cr-mig-sw].
+ - MDN - WebExtensions manifest.json keys reference (Firefox key matrix,
+ `browser_specific_settings`) - [mdn-manifest].
+ - Field shapes + full key matrix + offscreen / keepalive detail -
+ [references/manifest-matrix.md](references/manifest-matrix.md).
+ - Consumed by: `web-ext-cli-mozilla`, `chrome-extension-test-loader`,
+ `playwright-extension-fixtures`, `mv2-to-mv3-migration-test-checklist`,
`extension-storage-test-author`.
+
+ [cr-mig-overview]: https://developer.chrome.com/docs/extensions/develop/migrate
+ [cr-mig-manifest]: https://developer.chrome.com/docs/extensions/develop/migrate/manifest
+ [cr-mig-sw]: https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers
+ [mdn-manifest]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json