Testland
Browse all skills & agents

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); references/ carry Mozilla's web-ext CLI (lint via addons-linter, run on firefox-desktop / firefox-android / chromium targets, deterministic build, AMO sign). Use as the manifest-surface reference when authoring extension tests across both browsers, or when driving Firefox runs / AMO signing with web-ext.

Install with skills.sh (any agent)

npx skills add testland/qa --skill manifest-v3-test-surface-reference
View source

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 behaviour is testable in unit, integration, or full-browser scope.

This skill is the pure reference consumed by playwright-extension-fixtures (and its reload-matrix / storage-tests references). The summary tables below are the quick lookup; exact MV2/MV3 field shapes, the full Firefox/Chrome key matrix, and the offscreen-document

For Playwright-driven MV3 popup / content-script fixtures and assertion recipes see playwright-extension-fixtures (Chromium-only; references/extension-surface-tests.md). 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.

Manifest fields that change between MV2 and MV3

Summary lookup; the exact MV2/MV3 shapes for background, host_permissions, and web_accessible_resources are in references/manifest-matrix.md.

FieldMV2MV3Test implication
manifest_version23First 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_actionbrowser_action or page_actionaction (unified)Popup-rendering tests target the unified action slot
permissionsAPI + host strings mixedAPI strings onlyAPI-permission tests stay; host-permission tests move (see next row)
host_permissions(did not exist)match patterns moved here from permissionsEach host pattern is a runtime permission prompt - testable as a user gesture flow
optional_host_permissions(did not exist)runtime-requestable hostsTests must drive permissions.request from a user gesture
web_accessible_resourcesflat string arrayarray of { resources: [...], matches: [...] } objectsCross-origin fetch of an extension resource is only allowed from a matching matches pattern
content_security_policystringobject with extension_pages / sandbox keysNo inline <script>, no remotely hosted code, no eval - testable via load-time CSP violations

Service-worker runtime restrictions (MV3-only test surface)

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.

ConstraintMV2MV3Test implication
DOM / windowavailableunavailableAnything touching DOM moves to an offscreen document (chrome.offscreen.createDocument)
XMLHttpRequestavailableunavailable - use fetch()XHR-using test fixtures must be rewritten
localStorageavailableunavailable - use chrome.storage.localTests asserting persisted state must use chrome.storage.* (see playwright-extension-fixtures references/storage-tests.md)
setTimeout / setIntervalreliablecancelled when worker terminates - use chrome.alarmsTests timing background work must use alarms, not timers
Listener registrationtop-level or asyncmust be synchronous at top levelAsync-registered listeners are "not guaranteed to work in Manifest V3"
Lifecyclepersistentephemeral (start -> run -> terminate, repeated)Globals reset; storage is source of truth

Two load-bearing quotes from cr-mig-sw (opens in new window): "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

The full 23-key availability matrix (MV2 / MV3 x Firefox / Chrome) and the browser_specific_settings.gecko example are in references/manifest-matrix.md. The divergences that most often break a cross-browser test:

  • 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

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 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-patternWhy it failsFix
Assuming MV3 permissions still accepts host match patternsLint passes; runtime drops them silentlyMove all host patterns to host_permissions
Testing background.scripts array under MV3Field doesn't exist in MV3; manifest fails to loadUse background.service_worker single string
Using localStorage in service-worker testsThrows in MV3Use chrome.storage.local (see playwright-extension-fixtures references/storage-tests.md)
Registering chrome.runtime.onMessage inside a promiseListener may not fire after worker restart in MV3Register synchronously at top level
Testing flat-string web_accessible_resources under MV3Resources unreachable from page contextUse object form { resources, matches }
Treating Firefox MV3 as Chrome MV3externally_connectable, offline_enabled not supported; browser_specific_settings requiredRun Firefox tests with web-ext (see references/web-ext-firefox.md) and gate Chrome-only assertions
Using setTimeout to delay background workCancelled on worker terminationUse chrome.alarms.create
Polling via 1s heartbeat to keep SW aliveChrome explicitly limits keepalive abuseRestructure 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 (opens in new window) 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 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.

References

Manifest matrix + field shapes - manifest-v3-test-surface-reference

View source (opens in new window)

Manifest matrix + field shapes - manifest-v3-test-surface-reference

Deep-lookup detail for the summary tables in SKILL.md: exact MV2/MV3 field shapes, the full Firefox/Chrome key matrix, and the two service-worker constructs (offscreen documents, keep-alive). Sources are consolidated in the link list at the bottom.

Exact field shapes

background

MV2:

{
  "background": {
    "scripts": ["backgroundContextMenus.js", "backgroundOauth.js"],
    "persistent": false
  }
}

MV3:

{
  "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

"Host permissions in Manifest V3 are a separate field; you don't specify them in "permissions" or in "optional_permissions"."

MV2:

"permissions": ["tabs", "bookmarks", "https://www.blogger.com/"],
"optional_permissions": ["unlimitedStorage", "*://*/*"]

MV3:

"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

MV2 (flat string array):

"web_accessible_resources": [
  "images/*",
  "style/extension.css",
  "script/extension.js"
]

MV3 (array of objects, each scoping resources to URL patterns or extension IDs):

"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.

Firefox vs Chrome manifest key matrix

Every documented manifest key with Firefox / Chrome availability and MV2 / MV3 status per mdn-manifest (opens in new window). Tests must gate on browser detection or split into per-browser fixtures when a key is "not supported" in one column.

KeyMV2MV3FirefoxChromeTest note
manifest_versionyesyesyesyesMandatory; first assertion
nameyesyesyesyesMandatory
versionyesyesyesyesMandatory; semver in Firefox AMO
actionnoyesyesyesUnified popup slot
browser_actionyesnoyes (MV2 only)yes (MV2 only)Renames to action in MV3
page_actionyesnoyes (MV2; different in MV3)yes (MV2 only)Firefox keeps a different page-action shape
backgroundyesyesyesyesShape changes per above
browser_specific_settingsyesyesyesnoTests asserting extension ID stability rely on this in Firefox
content_scriptsyesyesyesyesSame shape
content_security_policyyesyesyesyesShape changes (object in MV3)
declarative_net_requestyesyesyesyesReplaces webRequest-blocking surface
externally_connectableyesyesnoyesCross-origin runtime messaging - Chrome only
host_permissionsnoyesyesyesNew in MV3; permission-prompt test surface
offline_enabledyesyesnoyesChrome-only manifest key
optional_host_permissionsnoyesyesyesRuntime host grants
optional_permissionsyesyesyesyesRuntime API grants
permissionsyesyesyesyesAPI permissions only in MV3
protocol_handlersyesyesyes (Firefox only)noFirefox-only register-protocol surface
sidebar_actionyesyesyes (Firefox/Opera)noSidebar UI - not in Chrome
storage (as manifest key)yesyesnoyesNote: the storage API works in both
theme_experimentyesyesyes (Firefox only)noExperimental theming
user_scripts (manifest key)yesnoyes (MV2 only)yes (MV2 only)MV3 replaces with userScripts API
web_accessible_resourcesyesyesyesyesShape changes (object array in MV3)

browser_specific_settings.gecko

Firefox-specific metadata (extension ID, min Firefox version) lives under browser_specific_settings.gecko. Chrome silently ignores this key:

{
  "browser_specific_settings": {
    "gecko": {
      "id": "@addon-example",
      "strict_min_version": "42.0"
    }
  }
}

Test note: AMO submission validation requires a stable gecko.id for signing - a test asserting the built zip carries a deterministic ID prevents accidental ID drift across builds.

Service-worker constructs

Offscreen documents

DOM-requiring work in MV3 goes to an offscreen document:

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

Offscreen documents communicate with the service worker via runtime.sendMessage / runtime.onMessage only - they don't share other extension APIs.

Keep-alive heartbeat

The keep-alive 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 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).

web-ext - Mozilla's CLI for Firefox WebExtension testing

View source (opens in new window)

web-ext - Mozilla's CLI for Firefox WebExtension testing

Companion reference for manifest-v3-test-surface-reference. Consult 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.

web-ext is Mozilla's reference CLI for WebExtension development. Per the mozilla/web-ext README (opens in new window) it bundles run, lint, sign, build, and docs; v8 added dump-config and reworked signing to use the AMO submission API by default (per the web-ext command reference (opens in new window)). --source-dir / -s is a global option; --artifacts-dir (default ./web-ext-artifacts) controls where build / sign write zips.

Install

npm install --save-dev web-ext    # per-project, preferred for CI determinism

Mozilla recommends the current Node LTS (per we-readme (opens in new window)). A web-ext-config.cjs file pins flags per run; web-ext dump-config (v8, per we-cmd (opens in new window)) prints the resolved configuration as JSON for diffing.

Lint

web-ext lint wraps mozilla/addons-linter (opens in new window) and emits AMO-compatible warnings. CI-friendly invocation:

web-ext lint \
  --source-dir ./dist \
  --output json \
  --pretty \
  --warnings-as-errors > lint-report.json

--warnings-as-errors / -w is the right CI default - per we-cmd (opens in new window) it escalates lint warnings (unsupported manifest fields, MV2-only APIs flagged for MV3) into a non-zero exit code. Other flags: --metadata, --self-hosted (disables AMO-related messages - never on AMO-bound builds), --boring.

Output shape: { "count", "summary": { "errors", "notices", "warnings" }, "metadata", "errors": [{ "code", "message", "file", "line", "column" }], ... } (per addons-linter; spot-check its changelog before pinning a parser). Parse with jq '.summary.errors + .summary.warnings'.

Run

web-ext run builds the extension, installs it into a fresh temporary Firefox profile, then watches the source directory and reloads on edit (per we-cmd (opens in new window)):

web-ext run \
  --source-dir ./dist \
  --firefox=firefox \
  --start-url 'about:debugging#/runtime/this-firefox'

--firefox aliases (per we-cmd (opens in new window)): firefox, beta, nightly, deved / firefoxdeveloperedition, or a binary path. To pin a profile: --firefox-profile=qa-profile --profile-create-if-missing --keep-profile-changes (the last persists profile modifications across runs per we-cmd (opens in new window)).

Targets (per we-cmd (opens in new window) --target): firefox-desktop, firefox-android (wired via the --adb-* flag family), and chromium:

web-ext run --source-dir ./dist --target chromium --chromium-binary "$(which chromium)"

The chromium target is the lowest-effort parity smoke-test; deep Chromium test surface lives in playwright-extension-fixtures.

Build

web-ext build -s ./dist -a ./build/artifacts --overwrite-dest \
  --filename 'my-extension-{version}.zip'

Per we-cmd (opens in new window), --filename defaults to {name}-{version}.zip; --overwrite-dest is required when the artefact path already exists. Build runs lint first and exits non-zero on lint failure.

Sign

Per we-cmd (opens in new window), web-ext sign v8 uses the AMO submission API by default and --channel is required:

export WEB_EXT_API_KEY='user:12345:1'
export WEB_EXT_API_SECRET='abcdef...'

web-ext sign -s ./dist --channel listed \
  --amo-metadata ./amo-metadata.json \
  --upload-source-code ./source.tar.gz

Channel semantics (quoted from we-cmd (opens in new window)): with listed the extension "gets submitted for public listing"; with unlisted it "gets submitted for signing for self-distribution." Key flags: --api-key / --api-secret (JWT for the AMO API), --amo-metadata (required for a first listed version), --timeout (default 300000 ms), --approval-timeout (default 900000 ms, v8) - AMO can take longer in queue, requiring follow-up polling. Update submissions require an extension ID in the manifest (browser_specific_settings.gecko.id) per we-cmd (opens in new window). The signed .xpi lands in --artifacts-dir.

Anti-patterns

Anti-patternWhy it failsFix
web-ext sign without --channelv8 requires it per we-cmd (opens in new window); command refuses to runAlways specify listed or unlisted
Re-using a profile without --keep-profile-changesProfile changes lost between runs; tests appear non-deterministicAdd the flag per we-cmd (opens in new window)
web-ext lint without -w in CIWarnings silently pass; AMO submission still rejects--warnings-as-errors
Signing without an extension ID in the manifestUpdate submission fails per we-cmd (opens in new window)Add browser_specific_settings.gecko.id (see SKILL.md matrix)
Committing web-ext-artifacts/Repo bloats with binaries.gitignore + CI artefact upload

Limitations

  • Linter is not tests. web-ext lint catches manifest issues and AMO policy violations; it does not exercise behaviour - pair with playwright-extension-fixtures.
  • Chromium target is run-only. No AMO-style signing path for Chrome; the Chrome Web Store has its own publishing flow.
  • No Firefox MV3 parity check. web-ext lint does not warn when a key is MV3-supported in Chrome but not Firefox (or vice versa); cross-reference the SKILL.md matrix manually.

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.

playwright-extension-fixtures

Author the Playwright fixture layer 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 - plus, in references/, the extension load / reload matrix (which edit re-evaluates which surface), the `chrome.storage` test suite (area selection, quota-exceeded, `storage.onChanged`, managed read-only), and the per-surface assertion recipes (popup, content script, background messaging, MV3 auto-suspend survival). Use when authoring or debugging the fixture a Chromium extension test imports, when an edit appears to have no effect and you need the reload matrix, or when authoring popup / content-script / storage tests.