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.
📖 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.
| Engine | Used by | Notes |
|---|---|---|
| Chromium | Google Chrome, Microsoft Edge, Opera | Playwright’s default |
| Firefox | Mozilla Firefox | Playwright’s patched build |
| WebKit | Apple Safari | Playwright’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.versionis the version of the browser that runs the test.el.typereturns 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 foronly_browser.- Skipped copies appear as
sin the progress line. Add-rsto 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_namekeeps browser-specific expectations in one readable place. - You can also call
pytest.skip("reason")inside the test after checkingbrowser_nameoris_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, orstorage_statefor 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
#datePickerlines show the real engine difference:datein Chromium and Firefox,textin WebKit. test_date_picker[webkit]was skipped byskip_browser; the CDP test and the Firefox class were skipped on the other engines byonly_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
browseris 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 msedgelaunched the Microsoft Edge installed on this Windows machine, so the version is Edge’s own version number.chromeselects 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=Truesends the value to thebrowser_type_launch_argsfixture 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-DEis a Chromium command-line switch, so the test is limited to Chromium withonly_browser.- Because
browser_type_launch_argsandbrowserare 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, sonavigator.languageisde, without any context option. - On Firefox the test was skipped by the
only_browsermarker. - The test id is
test_browser_started_in_german[chromium-browser_type_launch_args0]: the parameter gets an automatic id. Passids=["german"]toparametrizefor a readable one.
📋 Which Tool for Which Need?
| Need | Use |
|---|---|
| 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 engine | browser_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 test | Override the browser_context_args fixture |
| Branded Chrome or Edge | --browser-channel chrome or msedge |
| Browser command-line switches for one test | Indirect 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
--browserruns 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_browserandonly_browserdecide where a test runs;-rsshows why tests were skipped.- Apply markers to classes or whole modules (
pytestmark), and branch onbrowser_namefor small differences. - The
browser_context_argsmarker changes context options for one test;--browser-channelruns branded Chrome or Edge. - Indirect parametrization of
browser_type_launch_argssets 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