Site icon QATechTools

Chrome Extension Offscreen Documents for QA: Test Hidden DOM Lifecycles

Chrome Extension Offscreen Documents for QA: Test Hidden DOM Lifecycles featured image

Chrome extension offscreen document testing matters when a Manifest V3 service worker needs DOM or window capabilities without opening a visible tab. The hidden document may parse HTML, work with blobs, migrate local storage, use the clipboard, or support another documented reason. If its lifecycle is wrong, users can see duplicate work, lost messages, stale state, or a feature that fails only after the service worker restarts.

This tutorial builds a disposable extension that sends synthetic HTML to one offscreen document, extracts a small summary with DOMParser, and returns the result through runtime messaging. The QA goal is to prove create, reuse, close, and recreate behavior under concurrency and restart—not merely to see one successful response.

What Chrome officially documents

The current chrome.offscreen API reference says offscreen documents give Manifest V3 extensions DOM access in a hidden document. The manifest needs the offscreen permission. The document URL must be a static HTML file bundled with the extension, it cannot receive focus, and its window.opener is always null.

An installed extension can normally have only one offscreen document open at a time. Split-mode incognito can have one in the normal profile and one in the incognito profile. Chrome also limits offscreen documents to the runtime extension API, so service-worker communication must use runtime messaging.

createDocument() requires a URL, one or more documented reasons, and a developer justification. Chrome’s lifecycle example checks existing offscreen contexts and uses one shared creation promise to avoid concurrent creation. closeDocument() explicitly closes the active document.

Define the QA contract

Use a synthetic parser task with no browsing data or production content:

For every approved input ID, return exactly one normalized DOM summary. At most one offscreen document may exist per eligible profile. Concurrent requests must share creation, each response must match its request, close must remove the context, and the next request must recreate a clean document.

Record the extension build, browser build, profile mode, extension ID, test ID, input hash, lifecycle events, message correlation ID, output, and user-visible result. Never log full page content, clipboard contents, credentials, or personal data.

Step 1: create the smallest Manifest V3 fixture

Use a disposable unpacked extension with manifest.json, service-worker.js, offscreen.html, and offscreen.js. Add only the required permission:

{
  "manifest_version": 3,
  "name": "Offscreen Lifecycle Lab",
  "version": "1.0.0",
  "background": { "service_worker": "service-worker.js" },
  "permissions": ["offscreen"]
}

Keep the fixture isolated from host permissions and external services. The input should be a hard-coded synthetic HTML string, such as a heading, two links, and a deliberately malformed tag. That makes expected output deterministic.

Step 2: guard document creation

Use chrome.runtime.getContexts() to look for the exact offscreen URL before creating it. Keep one shared promise so two near-simultaneous requests do not both call createDocument():

let creating;

async function ensureOffscreen(path) {
  const url = chrome.runtime.getURL(path);
  const contexts = await chrome.runtime.getContexts({
    contextTypes: ['OFFSCREEN_DOCUMENT'],
    documentUrls: [url]
  });
  if (contexts.length) return 'reused';
  if (!creating) {
    creating = chrome.offscreen.createDocument({
      url: path,
      reasons: ['DOM_PARSER'],
      justification: 'Parse synthetic QA fixture markup'
    }).finally(() => { creating = null; });
  }
  await creating;
  return 'created';
}

The returned label is test instrumentation, not a Chrome guarantee. Capture it with a correlation ID, then verify the actual context count. Do not rely on a global variable for durable business state because extension service workers are ephemeral.

Step 3: keep messaging explicit

After ensureOffscreen() resolves, send a runtime message containing a request ID, target, operation, and sanitized payload. The offscreen listener should ignore messages for other targets, parse the fixture, and return only the expected fields.

{
  "requestId": "OFF-007",
  "target": "offscreen",
  "operation": "summarize-fixture",
  "html": "<h1>QA</h1><a href='/a'>A</a>"
}

Validate request ID, allowed operation, payload size, and response schema. Treat HTML as data: do not execute inline scripts or return arbitrary markup. The final response can contain heading text, link count, parse warnings, and the same request ID.

Step 4: prove the single-document invariant

Trigger 20 requests together from the service worker. Add unique correlation IDs but use the same offscreen URL. Expected results:

Repeat the burst after a clean browser restart and after forcing a service-worker restart. Chrome’s migration guidance explains that service workers terminate when unused and that state should not depend on globals. The creation guard must recover from a fresh worker while still detecting an existing offscreen document.

Step 5: test close and recreate

Call chrome.offscreen.closeDocument() only after all in-flight requests finish. Query contexts until the document is absent, then send a new request. It should create a fresh document and return the correct result.

Stage Expected context Expected result
Before first request None No output
After setup One matching document Parsed summary
Concurrent burst Still one Every ID returned once
After close None No new messages accepted
Next request One new document Fresh correct summary

Add a race where close begins while a request is pending. Your application must define whether to drain, reject, or retry. Assert that behavior explicitly; do not accept random success.

Step 6: test invalid configuration

  1. Remove the offscreen permission.
  2. Point the URL to a missing file.
  3. Use a URL that is not bundled with the extension.
  4. Send an unsupported operation.
  5. Send malformed and oversized payloads.
  6. Call an extension API other than runtime inside the offscreen document.
  7. Send a message before creation resolves.
  8. Close when no document is active.

For every case, require a bounded error with request ID, phase, safe message, and no leaked payload. Check the extension service-worker console and the offscreen context separately because errors may surface in different places.

Step 7: validate lifecycle reasons

Use the reason that matches the real DOM capability. For this lab, DOM_PARSER is clearer than a broad or unrelated reason. Chrome documents different reasons and notes that the audio-playback reason has a special inactivity lifetime, while other reasons do not define that same automatic timeout.

Test the actual production reason in a separate matrix. A passing parser test does not validate clipboard permission behavior, media prompts, iframe restrictions, or audio lifetime.

Step 8: automate without overfitting to internals

Chrome’s extension end-to-end testing guide recommends testing user-visible behavior and avoiding unnecessary dependence on internal state. For this hidden lifecycle, use internal context checks as targeted diagnostic assertions, but keep the primary assertion on the feature result a user receives.

Load the unpacked extension in a clean test profile, keep a stable test extension ID where needed, and exercise the same trigger a user uses. Capture internal lifecycle events only in test builds. Do not ship verbose payload logging.

Step 9: test service-worker termination realistically

Some automation frameworks can keep extension service workers alive because a debugger is attached. Chrome explicitly warns about this. Include a termination-capable test path and a manual idle-recovery check so your suite does not create false confidence.

After termination, the new worker should reconstruct state from supported storage, query the existing offscreen context, and continue without creating a duplicate. If the offscreen document was closed too, the next request should recreate it cleanly.

Release checklist

Official sources

Offscreen documents solve a real Manifest V3 gap, but hidden does not mean simple. A dependable QA strategy tests lifecycle state, concurrent creation, correlated messaging, closure, restart recovery, privacy, and user-visible behavior together.


Exit mobile version