Chrome Extension tab context menus for QA create a deceptively risky test surface. Starting in Chrome 150, an extension can add an action to the browser tab strip’s right-click menu. That is convenient for tab managers, evidence collectors, and QA utilities, but the meaning of one click becomes ambiguous when several tabs are highlighted.

This tutorial builds a disposable Manifest V3 fixture, separates the tab that opened the menu from the currently highlighted set, and tests safe multi-tab actions. The goal is not merely to prove that a menu item appears. It is to prove that the extension targets the intended tabs once, survives service-worker restarts, avoids unnecessary access, and asks for confirmation before destructive changes.

What Chrome officially documents

Chrome’s What’s New page says Chrome 150 adds the tab context to chrome.contextMenus. The current contextMenus reference lists tab in ContextType, requires the contextMenus permission, and documents an onClicked listener that receives click information plus one optional tabs.Tab.

The official Chromium Extensions announcement clarifies that, when multiple tabs are selected, the listener’s tab argument identifies the specific tab that was right-clicked. Chrome’s What’s New wording also refers to selected tabs, so a careful implementation should not guess that one callback object is the complete bulk target list. Record the clicked tab and, only when your product intends a bulk action, query the highlighted set explicitly.

Build a minimal private fixture

Create an unpacked extension that performs a harmless operation: it writes a synthetic QA marker to local extension storage for each intended tab. It does not close, move, mute, discard, group, or reload tabs. Those mutations come later, after the targeting logic is proven.

{
  "manifest_version": 3,
  "name": "Tab Context QA Lab",
  "version": "1.0.0",
  "permissions": ["contextMenus", "storage"],
  "background": { "service_worker": "worker.js", "type": "module" },
  "icons": { "16": "icons/menu-16.png" }
}

Do not add tabs or broad host permissions just to make the first test easy. The Tabs API reference explains that many tab operations need no extra permission, while sensitive fields such as URL, title, pending URL, and favicon require tabs or matching host access. Test the least-privilege contract before expanding it.

Step 1: register one menu item deterministically

Register one stable item during installation. Chrome notes that creation errors may be reported through the callback and runtime.lastError, so capture the result instead of assuming that a returned ID proves success.

const MENU_ID = 'qa-mark-targets';

chrome.runtime.onInstalled.addListener(async () => {
  await chrome.contextMenus.removeAll();
  chrome.contextMenus.create(
    { id: MENU_ID, title: 'Mark selected tabs for QA', contexts: ['tab'] },
    () => {
      const error = chrome.runtime.lastError?.message;
      chrome.storage.local.set({ menuRegistration: error ? { ok: false, error } : { ok: true } });
    }
  );
});

Test fresh install, extension reload, browser restart, update, downgrade, and disabled-then-enabled states. Verify that one visible item maps to one stable ID. Add a negative fixture with a duplicate ID and another that requests tab on an unsupported browser. Your evidence should distinguish registration failure, missing permission, unsupported context, and a listener that never ran.

Step 2: define the targeting contract

Write the product rule before writing the handler. A safe rule is: the right-clicked tab is the anchor; if more than one tab is highlighted in that same window, show a preview and require confirmation before acting on the highlighted set. Never extend the command to highlighted tabs in another window.

The tabs.Tab.highlighted property records selection, and tabs.query({ highlighted: true, windowId }) returns matching tabs. Query immediately after the click because the selection may change while the confirmation UI is open.

async function collectTargets(clickedTab) {
  if (!clickedTab?.id || !Number.isInteger(clickedTab.windowId)) {
    return { status: 'invalid-click-target', targets: [] };
  }

  const highlighted = await chrome.tabs.query({
    highlighted: true,
    windowId: clickedTab.windowId
  });

  const includesAnchor = highlighted.some(tab => tab.id === clickedTab.id);
  return {
    status: includesAnchor ? 'ready' : 'selection-changed',
    anchorId: clickedTab.id,
    targets: highlighted.filter(tab => Number.isInteger(tab.id))
  };
}

Do not silently add the anchor when the selection no longer includes it. That hides a race. Return a visible selection-changed state, ask the tester to retry, and log both snapshots.

Step 3: freeze the reproducibility evidence

For every run, record Chrome channel and full build, OS, clean profile ID, extension version and ID, manifest and worker hashes, menu ID and title, permission state, incognito mode, clicked tab ID, clicked window ID, highlighted tab IDs before preview and before execution, operation name, confirmation state, idempotency key, start and finish timestamps, per-tab result, and final visible tab-strip state.

Tab IDs are session-scoped evidence, not durable identity. A tab can navigate, close, move, become discarded, enter a group, or change selection between click and action. Re-fetch every target by ID and validate the operation’s preconditions immediately before mutation.

Step 4: cover the single-tab baseline

Begin with one normal tab in one window. Right-click it, choose the extension item, and verify that the callback anchor matches the visible tab. Confirm that the storage record is written once and contains no URL or title when the extension does not have access to those fields.

Repeat on an active tab, background tab, pinned tab, grouped tab, a loading tab, a discarded tab, and a frozen tab where your test build supports that state. Add internal Chrome pages, extension pages, a local file with file access disabled, and an incognito window where the extension is not allowed. The menu can be visible in contexts where a later operation is restricted, so visibility is not authorization.

Step 5: test the multi-selection matrix

Scenario Expected targeting evidence Failure to detect
One highlighted tab Anchor and selection contain the same ID Wrong background tab
Three contiguous tabs Exactly three IDs in one window Only anchor handled
Noncontiguous selection All and only highlighted IDs Range assumption
Right-click different selected tab New anchor, same intended set Active-tab assumption
Selection changes before confirm Preview invalidated or refreshed Stale mutation
Two browser windows Only clicked window queried Cross-window action

Verify ordering separately from membership. If your action preserves tab-strip order, sort using the latest index from the same window. Do not rely on promise completion order to map results back to tabs.

Step 6: make every action idempotent

A user can double-click, the worker can restart, and a message response can be lost after the action completed. Generate one command ID at preview time, persist it, and store a per-tab completion record. A retry with the same command ID must not repeat a destructive operation.

async function markOnce(commandId, tabId) {
  const key = `command:${commandId}:tab:${tabId}`;
  const existing = await chrome.storage.local.get(key);
  if (existing[key]?.status === 'done') return { tabId, status: 'duplicate-suppressed' };

  await chrome.storage.local.set({ [key]: { status: 'started' } });
  await chrome.storage.local.set({ [key]: { status: 'done', completedAt: Date.now() } });
  return { tabId, status: 'done' };
}

For partial failure, report each tab independently. Do not say that a batch succeeded because two of three tabs changed. Define whether rollback is possible and safe. Closing a tab is not reliably reversible, so require stronger confirmation and preserve enough metadata for a user-facing result without promising restoration.

Step 7: test worker termination and races

Manifest V3 workers are temporary. Terminate the worker after preview, after confirmation, after the first per-tab result, and just before the completion marker. Restart it and resend the same command ID. Confirm that finished targets are not repeated and unfinished targets either resume safely or end in an explicit recovery state.

Run two windows concurrently, double-click the menu item, close one target during confirmation, navigate another tab, move one into a different window, and remove the extension mid-batch. Correlate every log entry with command ID and tab ID. Add delays through controllable test hooks or barriers rather than timing guesses.

Step 8: validate destructive-action UX

Once the harmless marker workflow is stable, create separate reviewed fixtures for mute, reload, move, group, discard, and close. The preview must name the operation and count. For high-impact actions, show the target list in tab-strip order and require a deliberate confirmation. A context-menu click is an invocation, not consent to an unexpectedly broad action.

  • Keep the clicked tab visibly identified as the anchor.
  • Show when the selected set changed after the menu opened.
  • Disable confirmation when a target becomes invalid.
  • Explain partial completion without hiding failed tabs.
  • Preserve pinned status, groups, window boundaries, and user ordering unless the command explicitly changes them.
  • Never infer trust from pageUrl or use the click to bypass host or incognito restrictions.

Step 9: test compatibility and updates

Run the fixture on the oldest supported Chrome and a controlled browser below Chrome 150. Decide whether the product raises minimum_chrome_version or feature-detects registration and offers another entry point. The minimum-version guide warns that older users cannot install a newer incompatible package and that existing users can silently stop receiving updates.

Test update paths from a version without the tab menu to one with it, and back again. Verify registration cleanup, menu duplication, saved in-progress commands, worker migration, and the fallback UI. Keep the support decision visible to release reviewers.

Step 10: drive a real-browser E2E flow

Chrome’s E2E guide recommends loading the built extension in a browser and asserting user-visible behavior. Use a clean profile and three synthetic local pages. Highlight two pages, open the real tab-strip menu, invoke the marker action, confirm the preview, and verify the final visible evidence view contains exactly those two tab IDs.

Automation support for native browser UI varies, so combine a controlled manual tab-strip step with automated verification of the extension page and stored evidence when necessary. Also note that some drivers keep extension workers alive; a passing E2E run is not proof that restart recovery works. Keep a dedicated termination test.

Release evidence matrix

  • Official Chrome 150 announcement and exact installed build
  • Manifest, worker, menu schema, and package hashes
  • Registration success and unsupported-build failure evidence
  • Single-tab and multi-selection membership results
  • Anchor-versus-highlighted-set snapshots
  • Cross-window, incognito, internal, file, pinned, grouped, discarded, frozen, and loading cases
  • Selection-change and close-or-navigate race results
  • Double-click, concurrent batch, partial failure, and idempotency evidence
  • Worker termination and same-command recovery
  • Destructive-action preview and human confirmation
  • Update, downgrade, fallback, and minimum-version behavior
  • Final visible tab-strip outcome and human release decision

Common mistakes

Treating the callback tab as the whole selection: it is the right-clicked tab, not a safe bulk-action contract.

Querying every window: multi-selection should stay within the clicked tab’s window unless the product explicitly asks for broader scope.

Capturing URLs without need: sensitive tab fields require additional access. Test IDs and visible state before adding permission.

Mutating stale targets: refresh membership and preconditions immediately before execution.

Relying on an in-memory lock: worker termination erases it. Persist command and per-tab status.

Using one confirmation for every action: closing or moving many tabs deserves a stronger gate than adding a harmless QA marker.

Final checklist

  • Confirm the tab context in the exact Chrome build.
  • Register one stable item and capture creation errors.
  • Record the right-clicked tab separately from the highlighted set.
  • Keep selection within the clicked window and refresh it before mutation.
  • Use least privilege and treat menu visibility as invocation, not authorization.
  • Persist command IDs and suppress duplicate side effects.
  • Exercise worker restart, selection races, closed tabs, and partial failure.
  • Preview destructive scope and require human confirmation.
  • Test unsupported builds, updates, downgrades, and fallback UX.
  • Compare structured logs with real visible tab-strip outcomes.

Conclusion

Chrome Extension tab context menus for QA should be tested as a targeting and lifecycle problem, not merely a menu-rendering feature. Freeze the environment, distinguish the anchor from the selection, revalidate every target, persist idempotency evidence, and gate destructive scope with a clear preview. That turns a convenient Chrome 150 surface into a predictable, reviewable workflow for QA engineers and SDETs.

Official references