Extension testing stops feeling like normal browser automation the moment your target is a popup, a content script, or a page modified by an extension. The browser still renders HTML, but the rules change: extension pages use chrome-extension:// URLs, popups are ephemeral, content scripts run in a different execution world than the page, and permissions determine whether the extension can even observe the page you are on.

The useful mental model is this: the test harness should load and inspect the extension, not pretend the extension is a normal web app. If you try to force Playwright or Selenium into a generic harness that ignores extension packaging, profiles, and popup lifecycles, the failures will look flaky when they are really setup bugs.

Bottom line

For test browser extensions in Playwright and Selenium, the right approach depends on what you are verifying:

  • Use Playwright when you need tight control over Chromium launch flags, multiple browser contexts, direct inspection of extension pages, and modern debugging primitives.
  • Use Selenium when your existing suite is already Selenium-based and the extension is only one part of a broader browser matrix, but expect more setup work and more manual coordination.
  • In both cases, keep the extension test harness small, isolate extension state, and separate popup testing from content-script testing.

The main source of flakiness is usually not the extension itself, it is the test environment: profile reuse, incorrect load flags, timing around popup closure, or trying to query an extension page with selectors meant for a regular tab.

What changes when the target is an extension

A normal web app test typically interacts with one top-level document. Extension tests add three distinct targets:

  1. Extension popup or side panel UI, which is a browser-managed surface and may disappear when focus changes.
  2. Content scripts, which inject behavior into another site’s page but do not live in the same execution world as the page itself.
  3. Extension-injected UI, such as buttons, banners, or overlays rendered into the host page.

That distinction matters because the browser security model does not make every object equally accessible from automation. A popup may require a dedicated extension URL. A content script may modify the DOM, but the test still has to navigate the host page that triggered it. A permissions problem can make the extension appear broken when the issue is simply that the site origin is not allowed in the manifest or runtime permissions.

The minimum setup you need for Chrome or Chromium

Most extension automation on Chromium starts with an unpacked extension directory and a fresh profile. The key point is to load the extension explicitly, rather than expecting the browser to discover it from a standard test run.

Playwright: launch Chromium with the extension loaded

Playwright documents extension testing in Chromium with a persistent context and the extension loaded from an unpacked directory. The exact launch shape is more constrained than a regular browser test because extensions need a persistent profile.

import { chromium } from 'playwright';

const userDataDir = ‘./tmp-profile’; const extensionPath = ‘./dist/my-extension’;

const context = await chromium.launchPersistentContext(userDataDir, {
  headless: false,
  args: [
    `--disable-extensions-except=${extensionPath}`,
    `--load-extension=${extensionPath}`,
  ],
});
const page = await context.newPage();
await page.goto('https://example.com');

This pattern comes from Playwright’s Chromium extension guidance, and it exists because extensions are tied to the browser profile rather than a disposable context. See the Playwright documentation for the framework entry point and extension-specific launch patterns in the Chromium docs.

Selenium: start Chrome with extension flags and a clean profile

Selenium can do the same job, but the setup is more manual because you are working through Chrome options rather than a specialized extension-aware launcher.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options() options.add_argument(‘–disable-extensions-except=./dist/my-extension’) options.add_argument(‘–load-extension=./dist/my-extension’) options.add_argument(‘–user-data-dir=./tmp-profile’)

driver = webdriver.Chrome(options=options) driver.get(‘https://example.com’)

Selenium’s core documentation covers browser configuration and driver setup, but extension load behavior still depends on Chrome/Chromium flags and a fresh profile. See the Selenium documentation for the broader driver model.

A compact decision table

Need Playwright Selenium
Load unpacked Chromium extension in a controlled harness Strong fit Possible, but more manual
Inspect extension popup or extension page Strong fit Works, but requires more custom wiring
Reuse existing WebDriver suite Not the default path Strong fit
Debugging around browser contexts and pages Better ergonomics More manual coordination
Non-Chromium browser coverage Limited for extension testing Also limited for extension-specific behavior

This table is intentionally narrow. If the test is about browser automation in general, Selenium and Playwright are broader than extension support. If the test is specifically about extension UI and injected behavior, the question is how much harness complexity you want to own.

Testing the popup without making it brittle

Popups are one of the most fragile parts of extension testing because they are not ordinary tabs. They can close when focus changes, they often open in a separate extension URL, and timing matters more than it does for a standard page.

A stable pattern is:

  1. Launch the extension.
  2. Navigate the host page that triggers the extension.
  3. Open the popup from the browser UI or the extension action.
  4. Find the popup page by its extension URL.
  5. Assert on visible behavior quickly, before the popup closes.

In Playwright, that usually means locating the extension page after it appears rather than assuming the popup is the current page.

const popup = context.pages().find(p => p.url().startsWith('chrome-extension://'));
if (!popup) throw new Error('popup not found');
await popup.getByText('Connected').isVisible();

The exact selector strategy depends on your popup markup. What matters is the target surface. Do not try to treat the popup like a page that was opened by window.open() and will remain stable indefinitely.

Testing content scripts and extension-injected UI

Content scripts are usually the easiest thing to misunderstand. The extension is not “owning” the page, it is augmenting it. Your test should therefore validate the visible result on the host page, not just whether the script file loaded.

A good content-script test checks three layers:

  • the host page state before injection,
  • the injected UI or DOM changes after the extension acts,
  • the interaction path that proves the injection is functional, not just present.

For example, if the extension adds a toolbar button to a site, assert that:

  • the button appears only on allowed origins,
  • clicking it changes the page in the expected way,
  • reloading the page preserves or restores the injected behavior if that is part of the product contract.

If the extension uses runtime messaging, add an assertion around the message-triggered result, not just the DOM node. A node can exist while the message channel is broken.

The permissions and URL traps that waste the most time

Many extension failures are really harness failures with a browser security shape.

1. The site origin is not allowed

If the manifest or runtime permissions do not include the page origin, the content script may never run. From the test’s point of view, the page looks empty of extension UI.

2. The page is correct, but the URL is wrong

Extension pages use a chrome-extension:// URL, not https://. If your test assumes a normal web origin, it will fail to find the popup or options page.

3. The profile is reused

A reused user profile can preserve extension state, auth state, and browser settings from a prior run. That can make a failing test appear random when the real problem is leftover state.

4. Headless mode is not equivalent to headed extension behavior

Extension behavior in Chromium has historically been more predictable in headed mode for load and UI work. If a popup or browser action is involved, confirm that the browser mode you are using is supported for your exact extension workflow before you blame the selector.

5. The content script ran, but in the wrong world

Page JavaScript and extension-injected JavaScript do not always share the same execution assumptions. If your test tries to inspect variables instead of visible DOM or message results, you can end up debugging the wrong layer.

When an extension test fails, ask first whether the extension loaded, then whether permissions allow injection, then whether the test is attached to the right browser surface.

Debugging strategy that keeps the harness small

The fastest way to reduce extension test noise is to split the suite into three layers:

Smoke tests

Verify the extension loads, the manifest is valid, and the popup or options page opens.

Injection tests

Verify that the content script appears on allowed pages and modifies the host page as expected.

Behavior tests

Verify real interactions, such as messaging, form filling, storage-backed state, or network-aware behavior.

This separation helps because each layer fails for different reasons. If a smoke test fails, the load flags or profile are wrong. If an injection test fails, permissions or origin matching are wrong. If a behavior test fails, the extension logic itself is the more likely issue.

A practical debugging checklist:

  • start with a fresh profile directory,
  • confirm the extension ID and chrome-extension:// page are discoverable,
  • print browser logs and console output from both the host page and the extension page,
  • assert on visible outcomes instead of internal state when possible,
  • keep one fixture or helper responsible for extension loading.

When Playwright is the better fit

I would choose Playwright when the team wants the cleanest extension harness with the least custom plumbing. Its browser context model, page inspection, and Chromium launch controls make it easier to keep extension tests isolated and understandable.

Playwright is also a better fit when:

  • the extension test suite needs to live beside broader web app tests,
  • you want easier debugging around multiple pages and contexts,
  • you need a smaller amount of test-specific glue around popup discovery.

That said, Playwright does not remove the special-case nature of extension testing. You still need to load the extension explicitly and manage the profile carefully.

When Selenium is the better fit

I would choose Selenium when the organization already has a Selenium standard, the extension is just one surface in a larger browser matrix, or the team is more comfortable maintaining WebDriver-based infrastructure than adding a second framework.

Selenium makes sense when:

  • existing test assets, CI runners, and reporting already depend on it,
  • the extension test volume is modest,
  • the team accepts more harness code in exchange for consistency with the rest of the suite.

The tradeoff is that Selenium generally asks you to assemble more of the extension plumbing yourself. That is not a blocker, but it is real maintenance cost.

Choose Playwright if…

  • you need a compact harness for Chromium extension work,
  • the suite will inspect popup pages or extension-injected UI often,
  • you want clearer control over browser contexts and fresh profiles,
  • reducing test setup complexity matters more than preserving a legacy framework.

Choose Selenium if…

  • your team already runs a large Selenium estate,
  • extension tests are a small part of a broader browser automation program,
  • you want to minimize framework churn even if the extension setup is more manual.

Not the best fit if…

This problem is a poor match for either framework if you need to validate behavior that depends on browser store installation flows, cross-device sync state, or OS-level integration outside the browser process. Those are different test surfaces and usually need a different layer of automation or a dedicated environment.

A simple rule for keeping extension tests maintainable

If the code in your test harness starts to look like a second extension loader, stop and shrink the abstraction. The harness should do four things well:

  1. launch a clean browser profile,
  2. load the unpacked extension,
  3. expose the extension page or popup URL,
  4. provide helpers for host-page assertions.

Everything else should stay in the test, where it is visible and reviewable.

That rule saves time because extension tests age poorly when the harness becomes a hidden framework inside the framework. The more logic you bury there, the harder it is to tell whether a failure came from the extension or from the runner.

FAQ

Can I test a Chrome extension in a normal incognito or regular browsing session?

Sometimes, but the safe default is a dedicated, fresh profile with the extension explicitly loaded. Otherwise you risk stale state and unpredictable extension availability.

Why does my popup disappear before the assertion runs?

Popup UIs are ephemeral and browser-managed. If focus changes or the popup loses its active state, the browser can close it. Interact quickly and target the popup page directly.

Why does the content script work manually but not in automation?

Check origin permissions, manifest matches, and whether the test is visiting the exact URL pattern that the extension is allowed to inject into. A manual session can hide a permissions mismatch if you are already on the right page.

Should I assert on DOM changes or internal extension state?

Prefer visible DOM and user-observable outcomes first. Internal state is useful for debugging, but it is easier to break with test harness changes.

Is Playwright always better for extension testing?

No. It is usually the cleaner choice for Chromium extension work, but Selenium can be the right decision if it preserves an existing test stack and the extension suite is small enough to justify the extra setup.