Browser Markers in pytest-playwright: skip_browser, only_browser and browser_context_args

One of Playwright’s biggest strengths is real cross-browser testing: the same test runs on Chromium, Firefox and WebKit. Sometimes, though, a test should not run everywhere: a feature is missing in one engine, or the test uses a Chromium-only API. pytest-playwright gives you markers for exactly that. In this lesson you will run a suite on all three engines, find a real browser difference on the practice page, and control where each test runs with skip_browser, only_browser, browser_context_args, browser channels and per-test launch options.

Advertisement

📖 What You’ll Learn: running tests on several browsers with –browser, reading the parametrized test ids, a real WebKit difference on the practice page, skip_browser and only_browser, showing skip reasons with -rs, markers on classes and modules, branching on browser_name, the browser_context_args marker for per-test context options, browser channels for Google Chrome and Microsoft Edge, and per-test launch options with indirect parametrization of browser_type_launch_args.


🌍 Running on Several Browsers

By default pytest-playwright runs every test on Chromium. Pass --browser once per engine to run on more:

⌨️ Terminal

pytest --browser chromium --browser firefox --browser webkit

Each --browser creates one copy of every browser test, with the engine in the test id: test_title[chromium], test_title[firefox], test_title[webkit]. Each engine gets its own browser process; every test still gets a fresh context.

EngineUsed byNotes
ChromiumGoogle Chrome, Microsoft Edge, OperaPlaywright’s default
FirefoxMozilla FirefoxPlaywright’s patched build
WebKitApple SafariPlaywright’s WebKit build runs on Windows, Linux and macOS

📝 Note: Playwright’s WebKit is close to Safari but not identical: the real Safari also depends on the Apple operating system it runs on. It is still the best way to catch WebKit-specific problems without a Mac.


🔎 A Real Difference Between Engines

All tests in this lesson use the SQA Tools practice page:

📄 tests/conftest.py

import pytest
from playwright.sync_api import Page


@pytest.fixture
def practice(page: Page) -> Page:
    page.goto("https://sqatools.in/automation-practice-page/")
    return page

The page has a date picker, <input type="date" id="datePicker">. This test asks each browser what kind of input it actually created:

📄 tests/test_cross_browser.py

import pytest
from playwright.sync_api import Page, expect


def test_title(practice: Page, browser_name):
    print(f"\n{browser_name} {practice.context.browser.version}")
    expect(practice).to_have_title("Automation Practice Page - SQA Tools")


def test_what_is_the_date_input(practice: Page, browser_name):
    input_type = practice.locator("#datePicker").evaluate("el => el.type")
    print(f"\n{browser_name}: #datePicker has type '{input_type}'")
  • practice.context.browser.version is the version of the browser that runs the test.
  • el.type returns the type the browser supports: a browser without a date control falls back to a plain text box and reports "text".

When this lesson was run on Windows, Chromium and Firefox reported date, but **WebKit reported text**: Playwright’s WebKit build on Windows has no native date picker (you will see this in the output below). A test that fills the date picker in the YYYY-MM-DD format and expects a date control would fail there for a reason that has nothing to do with your application.


⏭️ skip_browser and only_browser

pytest-playwright registers two markers for such cases:

📄 tests/test_cross_browser.py

@pytest.mark.skip_browser("webkit")
def test_date_picker(practice: Page):
    practice.locator("#datePicker").fill("2026-11-01")
    expect(practice.locator("#datePicker")).to_have_value("2026-11-01")


@pytest.mark.only_browser("chromium")
def test_chromium_devtools_protocol(practice: Page):
    # CDP sessions exist only in Chromium-based browsers
    cdp = practice.context.new_cdp_session(practice)
    version = cdp.send("Browser.getVersion")
    print("\nCDP product:", version["product"].split("/")[0])
  • @pytest.mark.skip_browser("webkit"): run on every browser except WebKit.
  • @pytest.mark.only_browser("chromium"): run only on Chromium; all other browsers are skipped.
  • new_cdp_session() talks to the Chrome DevTools Protocol, which only Chromium-based browsers support. That makes it a genuine reason for only_browser.
  • Skipped copies appear as s in the progress line. Add -rs to the pytest command to list each skip with its reason.

⚠️ Watch out: Use these markers for real engine limitations, not to hide bugs. If your application is broken in Safari, a skipped test means your users find the bug instead of you. Mark such tests with xfail and a ticket number, or fix the application.

Markers on a whole class or module

Markers can be applied to a class, and to a whole file with pytestmark:

📄 tests/test_firefox_suite.py

import pytest
from playwright.sync_api import Page, expect


@pytest.mark.only_browser("firefox")
class TestFirefoxSpecific:
    def test_user_agent(self, practice: Page):
        expect(practice.locator("body")).to_be_visible()
        assert "Firefox" in practice.evaluate("navigator.userAgent")

    def test_engine_name(self, practice: Page, browser_name):
        assert browser_name == "firefox"

For a whole module, write pytestmark = pytest.mark.skip_browser("webkit") at the top of the file.

Branching inside a test with browser_name

When only one step differs, skip that part or adapt the expected value instead of skipping the whole test:

📄 tests/test_cross_browser.py

ENGINE_IN_USER_AGENT = {"chromium": "Chrome", "firefox": "Firefox", "webkit": "AppleWebKit"}


def test_user_agent_matches_engine(practice: Page, browser_name):
    user_agent = practice.evaluate("navigator.userAgent")
    assert ENGINE_IN_USER_AGENT[browser_name] in user_agent
  • A dictionary keyed by browser_name keeps browser-specific expectations in one readable place.
  • You can also call pytest.skip("reason") inside the test after checking browser_name or is_webkit. Prefer the markers when the whole test does not apply: the decision is then visible without reading the test body.

🎛️ The browser_context_args Marker

You met this marker in Lesson 6. It adds or overrides context options for one test (or one class), on top of the browser_context_args fixture:

📄 tests/test_context_marker.py

import pytest
from playwright.sync_api import Page


@pytest.mark.browser_context_args(locale="de-DE", timezone_id="Europe/Berlin")
def test_german_user(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    language = page.evaluate("navigator.language")
    zone = page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
    print(f"\nlanguage={language} timezone={zone}")
    assert (language, zone) == ("de-DE", "Europe/Berlin")
  • The marker’s keyword arguments are passed to browser.new_context() for this test’s context.
  • Typical uses: a different locale or time zone, color_scheme="dark", permissions=["geolocation"], a mobile viewport, or storage_state for a test that should start logged in.

Now run the whole tests folder on all three engines, with -rs to show skip reasons:

⌨️ Terminal

pytest -q -s -rs tests --browser chromium --browser firefox --browser webkit

🖥️ Output

language=de-DE timezone=Europe/Berlin
.
chromium 156.0.8078.4
.
chromium: #datePicker has type 'date'
..
CDP product: HeadlessChrome
..ss
language=de-DE timezone=Europe/Berlin
.
firefox 157.0
.
firefox: #datePicker has type 'date'
..s...
language=de-DE timezone=Europe/Berlin
.
webkit 27.2
.
webkit: #datePicker has type 'text'
.ss.ss
=========================== short test summary info ===========================
SKIPPED [2] ..\_venv0\Lib\site-packages\pytest_playwright\pytest_playwright.py:338: skipped for this browser: chromium
SKIPPED [1] ..\_venv0\Lib\site-packages\pytest_playwright\pytest_playwright.py:338: skipped for this browser: firefox
SKIPPED [4] ..\_venv0\Lib\site-packages\pytest_playwright\pytest_playwright.py:338: skipped for this browser: webkit
17 passed, 7 skipped, 1 warning in 38.09s

Read the result:

  • The #datePicker lines show the real engine difference: date in Chromium and Firefox, text in WebKit.
  • test_date_picker[webkit] was skipped by skip_browser; the CDP test and the Firefox class were skipped on the other engines by only_browser. The reason for each skip is listed in the short test summary, because of -rs.
  • pytest groups the run by browser: all Chromium tests first, then Firefox, then WebKit, because each engine’s browser is a separate session fixture.
  • The skips add up: Chromium skipped the two Firefox-only tests, Firefox skipped the CDP test, and WebKit skipped the date picker, the CDP test and the two Firefox-only tests.
  • The reason text is fixed by the plugin (“skipped for this browser: …”) and points at the plugin’s code line, not at your test.

🏷️ Browser Channels: Google Chrome and Microsoft Edge

Playwright’s Chromium is an open-source build. To test with the branded browsers installed on the machine, pass a channel:

⌨️ Terminal

pytest -q -s tests/test_cross_browser.py::test_title --browser-channel msedge

🖥️ Output

chromium 156.0.4314.42
.
1 passed, 1 warning in 2.70s
  • --browser-channel msedge launched the Microsoft Edge installed on this Windows machine, so the version is Edge’s own version number. chrome selects Google Chrome; beta and dev channels exist too (chrome-beta, msedge-beta, …).
  • The test id still says [chromium], because a channel is a Chromium-based browser.
  • A channel uses the browser installed on the machine. Install it there first (for Chrome you can also run playwright install chrome).

🚀 Per-Test Launch Options

Context options change the page environment (locale, viewport, permissions). Launch options change the browser process itself: command-line arguments, slow_mo, headless, channel. The suite-wide way to set them is overriding the browser_type_launch_args fixture (Lesson 6). For a single test, parametrize that fixture indirectly:

📄 launch/test_launch_args.py

import pytest
from playwright.sync_api import Page


@pytest.mark.only_browser("chromium")
@pytest.mark.parametrize(
    "browser_type_launch_args",
    [{"args": ["--lang=de-DE"]}],
    indirect=True,
)
def test_browser_started_in_german(page: Page, browser_type_launch_args):
    print("\nlaunch args:", browser_type_launch_args)
    print("navigator.language:", page.evaluate("navigator.language"))
  • indirect=True sends the value to the browser_type_launch_args fixture instead of passing it straight to the test. In pytest-playwright 0.10, the fixture merges it over the command-line launch options.
  • --lang=de-DE is a Chromium command-line switch, so the test is limited to Chromium with only_browser.
  • Because browser_type_launch_args and browser are session-scoped, each distinct value launches its own browser. That is slower, so keep per-test launch options for the few tests that really need them.

⌨️ Terminal

pytest -q -s -rs launch --browser chromium --browser firefox

🖥️ Output

launch args: {'args': ['--lang=de-DE']}
navigator.language: de
.s
=========================== short test summary info ===========================
SKIPPED [1] ..\_venv0\Lib\site-packages\pytest_playwright\pytest_playwright.py:338: skipped for this browser: firefox
1 passed, 1 skipped, 1 warning in 0.57s
  • The browser started with --lang=de-DE, so navigator.language is de, without any context option.
  • On Firefox the test was skipped by the only_browser marker.
  • The test id is test_browser_started_in_german[chromium-browser_type_launch_args0]: the parameter gets an automatic id. Pass ids=["german"] to parametrize for a readable one.

📋 Which Tool for Which Need?

NeedUse
Run the suite on several engines--browser chromium --browser firefox --browser webkit
A test that cannot work on one engine@pytest.mark.skip_browser("webkit")
A test that uses an engine-specific API@pytest.mark.only_browser("chromium")
One step that differs per enginebrowser_name, is_webkit and friends inside the test
Locale, time zone, viewport, permissions for one test@pytest.mark.browser_context_args(...)
The same for every testOverride the browser_context_args fixture
Branded Chrome or Edge--browser-channel chrome or msedge
Browser command-line switches for one testIndirect parametrization of browser_type_launch_args

❓ Frequently Asked Questions

🤔 How do I run Playwright Python tests on all browsers?

Pass --browser once per engine: pytest --browser chromium --browser firefox --browser webkit. Put it in addopts in pytest.ini to make it the default.

🤔 What is the difference between skip_browser and only_browser?

skip_browser("webkit") excludes one engine and runs on the rest. only_browser("chromium") runs on one engine and skips all others.

🤔 Why is my only_browser test always skipped?

It only runs when that browser is part of the run. With only_browser("firefox") and no --browser firefox, every copy of the test is skipped.

🤔 Is WebKit on Windows the same as Safari?

No. It is Playwright’s build of the WebKit engine, so it behaves like Safari in most respects but not all, as the date input in this lesson shows. Run important Safari checks on macOS as well.

🤔 Can I set a different viewport for one test?

Yes: @pytest.mark.browser_context_args(viewport={"width": 390, "height": 844}), or create a context with new_context(viewport=...) inside the test.


🏁 Summary

  • --browser runs every browser test once per engine; the engine appears in the test id.
  • Engines differ: on Windows, WebKit has no native date input on the practice page.
  • skip_browser and only_browser decide where a test runs; -rs shows why tests were skipped.
  • Apply markers to classes or whole modules (pytestmark), and branch on browser_name for small differences.
  • The browser_context_args marker changes context options for one test; --browser-channel runs branded Chrome or Edge.
  • Indirect parametrization of browser_type_launch_args sets launch options for one test, at the cost of a separate browser launch.

Next, you will deal with tests that fail only sometimes: why tests become flaky, and how to retry them with pytest-rerunfailures without hiding real problems.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

Leave a Comment