pytest-playwright Built-in Fixtures Explained (page, context, browser and more)

Every Playwright test you have written so far started with a page parameter. Behind that one word, the pytest-playwright plugin builds a chain of fixtures: it starts Playwright, launches a browser, creates a fresh browser context and opens a page, then cleans everything up. In this lesson you will see the whole chain with pytest’s –setup-show, prove that each test is isolated, and use the other built-in fixtures: new_context, browser, browser_name, is_chromium, device, output_path, base_url and the playwright object itself.

Advertisement

๐Ÿ“– What You’ll Learn: the fixture chain from playwright to page, which fixtures are session-scoped and which run per test, watching setup and teardown with –setup-show, how a fresh context isolates cookies and storage, new_context for several users or settings in one test, using browser for contexts you manage yourself, browser_name and the is_chromium, is_firefox and is_webkit flags, browser_channel and device, output_path, base_url, and the playwright fixture for devices and API requests.


๐Ÿ”— The Fixture Chain Behind page

When a test asks for page, pytest-playwright resolves a chain of fixtures, each built on the one before:

FixtureScopeWhat it gives you
playwrightsessionThe started Playwright object (sync_playwright().start())
browser_typesessionplaywright.chromium, .firefox or .webkit, chosen by --browser
browsersessionOne launched browser, shared by all tests of that browser
new_contextfunctionA function that creates browser contexts and closes them after the test
contextfunctionA fresh BrowserContext for this test, made with new_context()
pagefunctionA new Page in that context

The two scopes are the key idea:

  • Session scope: started once for the whole run. Launching a browser takes time, so all tests share one browser process.
  • Function scope: created for each test. Every test gets a brand-new context, which is like a new incognito window: no cookies, no localStorage, no cache from other tests.

So tests are fast (one browser) and isolated (a fresh context each).

Watching the chain with –setup-show

pytest’s --setup-show option prints every fixture as it is set up (SETUP) and torn down (TEARDOWN), with its scope: S for session, F for function.

๐Ÿ“„ tests/conftest.py

import pytest
from playwright.sync_api import Page

URL = "https://sqatools.in/automation-practice-page/"


@pytest.fixture
def practice(page: Page) -> Page:
    page.goto(URL)
    return page

โŒจ๏ธ Terminal

pytest -q --setup-show tests/test_isolation.py::test_first_visit

๐Ÿ–ฅ๏ธ Output

SETUP    S pytestconfig
SETUP    S delete_output_dir (fixtures used: pytestconfig)
SETUP    S browser_type_launch_args (fixtures used: pytestconfig)
SETUP    S _pw_api_request_contexts (fixtures used: pytestconfig)
SETUP    S playwright (fixtures used: _pw_api_request_contexts)
SETUP    S browser_name (fixtures used: pytestconfig)['chromium']
SETUP    S browser_type (fixtures used: browser_name, playwright)
SETUP    S connect_options
SETUP    S launch_browser (fixtures used: browser_type, browser_type_launch_args, connect_options)
SETUP    S browser (fixtures used: launch_browser)
SETUP    S device (fixtures used: pytestconfig)
SETUP    S base_url (fixtures used: pytestconfig)
SETUP    S _pw_artifacts_folder (fixtures used: pytestconfig)
SETUP    S browser_context_args (fixtures used: _pw_artifacts_folder, base_url, device, playwright, pytestconfig)
        SETUP    F _pw_trace_api_requests
        SETUP    F output_path (fixtures used: pytestconfig)
        SETUP    F _artifacts_recorder (fixtures used: _pw_api_request_contexts, _pw_artifacts_folder, output_path, playwright, pytestconfig)
        SETUP    F new_context (fixtures used: _artifacts_recorder, browser, browser_context_args)
        SETUP    F context (fixtures used: new_context)
        SETUP    F page (fixtures used: context)
        SETUP    F practice (fixtures used: page)
        tests/test_isolation.py::test_first_visit[chromium] (fixtures used: _artifacts_recorder, _pw_api_request_contexts, _pw_artifacts_folder, _pw_trace_api_requests, base_url, browser, browser_context_args, browser_name, browser_type, browser_type_launch_args, connect_options, context, delete_output_dir, device, launch_browser, new_context, output_path, page, playwright, practice, pytestconfig, request) .
        TEARDOWN F practice
        TEARDOWN F page
        TEARDOWN F context
        TEARDOWN F new_context
        TEARDOWN F _artifacts_recorder
        TEARDOWN F output_path
        TEARDOWN F _pw_trace_api_requests
TEARDOWN S browser_context_args
TEARDOWN S _pw_artifacts_folder
TEARDOWN S base_url
TEARDOWN S device
TEARDOWN S browser
TEARDOWN S launch_browser
TEARDOWN S connect_options
TEARDOWN S browser_type
TEARDOWN S browser_name['chromium']
TEARDOWN S playwright
TEARDOWN S _pw_api_request_contexts
TEARDOWN S browser_type_launch_args
TEARDOWN S delete_output_dir
TEARDOWN S pytestconfig
1 passed, 1 warning in 1.90s

Reading the output from top to bottom:

  • The session fixtures (playwright, browser_type, browser, browser_context_args and others) are set up first. Fixtures whose names start with _pw are the plugin’s internal helpers; you never use them directly.
  • browser_name is shown with ['chromium']: pytest-playwright parametrizes every browser test by browser name, which is why test ids end in [chromium].
  • The function fixtures new_context, context and page are created just for this test, then your practice fixture runs on top of page.
  • After the test (the .), teardown runs in reverse order: first the function fixtures, and at the end of the session the browser and Playwright.

๐Ÿงผ Proving Test Isolation

These two tests run one after the other in the same browser. The first one stores data in the browser; the second checks that none of it is left:

๐Ÿ“„ tests/test_isolation.py

from playwright.sync_api import Browser, BrowserContext, Page

SEEN = {}


def test_first_visit(practice: Page, context: BrowserContext, browser: Browser):
    practice.evaluate("localStorage.setItem('cart', '3 items')")
    context.add_cookies([{"name": "session", "value": "abc123", "url": practice.url}])

    SEEN["browser"], SEEN["context"] = browser, context
    print("\nstored:", practice.evaluate("localStorage.getItem('cart')"), "| cookies:", len(context.cookies()))


def test_second_visit(practice: Page, context: BrowserContext, browser: Browser):
    print("\nleft over:", practice.evaluate("localStorage.getItem('cart')"),
          "| cookies named session:", [c for c in context.cookies() if c["name"] == "session"])
    print("same browser:", browser is SEEN["browser"], "| same context:", context is SEEN["context"])
  • SEEN is a plain module-level dictionary used only to compare objects between the two tests.
  • context.add_cookies() adds a cookie; context.cookies() lists the cookies of the context. The count of 7 includes cookies that the live site set itself while loading.
  • localStorage is read and written with page.evaluate(), which runs JavaScript in the page.

Part of the output of pytest -q -s tests (the full run is at the end of this section):

๐Ÿ–ฅ๏ธ Output

stored: 3 items | cookies: 7
.
left over: None | cookies named session: []
same browser: True | same context: False

The second test sees no localStorage value and no session cookie, because it runs in a new context. The browser object, however, is the very same one.

๐Ÿ’ก Tip: Isolation is why Playwright tests can run in any order and in parallel. If one of your tests only passes after another test has run, it secretly depends on that test’s state: fix it with a fixture that sets up what it needs.


๐Ÿ‘ฅ new_context: Several Users in One Test

Some scenarios need two independent sessions at once: a buyer and a seller, an admin and a user, a chat between two people. The new_context fixture is a function: each call creates another context, using the same settings as context (browser_context_args), plus any options you pass.

๐Ÿ“„ tests/test_new_context.py

from playwright.sync_api import Page


def test_two_users(new_context):
    alice = new_context().new_page()
    bob = new_context(locale="de-DE", viewport={"width": 800, "height": 600}).new_page()

    alice.goto("https://sqatools.in/automation-practice-page/")
    bob.goto("https://sqatools.in/automation-practice-page/")
    alice.evaluate("localStorage.setItem('user', 'alice')")

    print("\nalice sees:", alice.evaluate("localStorage.getItem('user')"))
    print("bob sees  :", bob.evaluate("localStorage.getItem('user')"))
    print("bob locale:", bob.evaluate("navigator.language"), "| bob viewport:", bob.viewport_size)


def test_page_and_extra_context(page: Page, new_context):
    # page uses one context; new_context() makes a second, independent one
    other = new_context().new_page()
    print("\ncontexts in the browser:", len(page.context.browser.contexts))
    assert page.context is not other.context
  • Each new_context() call returns a new, isolated BrowserContext.
  • Options passed to the call, such as locale or viewport, are added to the configured browser_context_args for that context only.
  • You do not close these contexts yourself: the fixture closes all of them after the test. They also get the plugin’s tracing, video and screenshot handling.

๐ŸŒ browser: Contexts You Manage Yourself

You can also call browser.new_context() directly. The difference: such a context does not use browser_context_args, does not get the plugin’s artifacts, and you must close it.

๐Ÿ“„ tests/test_browser_fixture.py

from playwright.sync_api import Browser


def test_manual_context(browser: Browser):
    context = browser.new_context(java_script_enabled=False)
    try:
        page = context.new_page()
        page.goto("https://sqatools.in/automation-practice-page/")
        print("\nbrowser:", browser.browser_type.name, browser.version.split(".")[0], "| JS enabled:", False)
    finally:
        context.close()
  • try / finally makes sure the context is closed even if the test fails.
  • Prefer new_context in tests. Use browser.new_context() when you deliberately want a context without the project’s default settings.

โŒจ๏ธ Terminal

pytest -q -s tests

๐Ÿ–ฅ๏ธ Output

browser: chromium 156 | JS enabled: False
.
stored: 3 items | cookies: 7
.
left over: None | cookies named session: []
same browser: True | same context: False
.
alice sees: alice
bob sees  : None
bob locale: de-DE | bob viewport: {'width': 800, 'height': 600}
.
contexts in the browser: 2
.
5 passed, 1 warning in 8.88s

๐Ÿงญ Information Fixtures: browser_name, is_*, channel and device

These session fixtures tell a test where it runs:

FixtureValue
browser_name"chromium", "firefox" or "webkit"
is_chromium, is_firefox, is_webkitTrue or False
browser_channelThe --browser-channel value, such as "chrome", or None
deviceThe --device value, such as "iPhone 15", or None
output_pathThis test’s artifact folder inside --output (function scope)

๐Ÿ“„ info/test_where_am_i.py

from playwright.sync_api import Page


def test_where_am_i(page: Page, browser_name, is_webkit, browser_channel, device, output_path):
    print(f"\nbrowser_name={browser_name} is_webkit={is_webkit} channel={browser_channel} device={device}")
    print("viewport:", page.viewport_size, "| iPhone user agent:", "iPhone" in page.evaluate("navigator.userAgent"))
    print("output_path ends with:", output_path.replace("\\", "/").split("/")[-1])

โŒจ๏ธ Terminal

pytest -q -s info --browser firefox --device "iPhone 15"

๐Ÿ–ฅ๏ธ Output

browser_name=firefox is_webkit=False channel=None device=iPhone 15
viewport: {'width': 393, 'height': 659} | iPhone user agent: True
output_path ends with: info-test-where-am-i-py-test-where-am-i-firefox
.
1 passed, 1 warning in 2.00s
  • --device "iPhone 15" applies the device’s viewport (393 x 659), user agent and touch support to every context, through browser_context_args.
  • The iPhone descriptor names WebKit as its default browser, but --browser decides which browser actually runs. Here Firefox ran with the iPhone viewport and user agent. For the most realistic iPhone emulation, run such tests on WebKit.
  • output_path is a folder name built from the test id. Nothing is written there unless an artifact is kept.

Now run on two browsers:

โŒจ๏ธ Terminal

pytest -q -s info --browser chromium --browser webkit

๐Ÿ–ฅ๏ธ Output

browser_name=chromium is_webkit=False channel=None device=None
viewport: {'width': 1280, 'height': 720} | iPhone user agent: False
output_path ends with: info-test-where-am-i-py-test-where-am-i-chromium
.
browser_name=webkit is_webkit=True channel=None device=None
viewport: {'width': 1280, 'height': 720} | iPhone user agent: False
output_path ends with: info-test-where-am-i-py-test-where-am-i-webkit
.
2 passed, 1 warning in 0.72s

Each --browser adds one parametrized copy of every test, so the test ran twice with a different browser_name.


๐Ÿ  base_url: Short URLs in Tests

The base_url fixture holds the value of --base-url (or base_url in pytest.ini, or the PYTEST_BASE_URL environment variable). pytest-playwright passes it to every context, so page.goto() accepts paths:

๐Ÿ“„ base/test_base_url.py

from playwright.sync_api import Page, expect


def test_relative_goto(page: Page, base_url):
    print("\nbase_url:", base_url)
    page.goto("/automation-practice-page/")                 # resolved against base_url
    expect(page).to_have_url("https://sqatools.in/automation-practice-page/")

โŒจ๏ธ Terminal

pytest -q -s base --base-url https://sqatools.in

๐Ÿ–ฅ๏ธ Output

base_url: https://sqatools.in
.
1 passed, 1 warning in 2.46s

๐Ÿ“ Note: The PytestRemovedIn10Warning you have seen in every run’s summary comes from this fixture: pytest-playwright 0.10 defines base_url as a method of a helper class, which pytest 9.2 reports as deprecated. It does not affect your tests. Silence it with a filterwarnings line in pytest.ini if it bothers you, as shown in Module 3.


๐ŸŽญ The playwright Fixture

The session-scoped playwright fixture is the same object you get from sync_playwright().start(). Two things you will use it for:

๐Ÿ“„ pw/test_playwright_fixture.py

def test_device_list(playwright):
    iphone = playwright.devices["iPhone 15"]               # a device descriptor (a dict)
    print(iphone["viewport"], iphone["default_browser_type"])

def test_api(playwright):
    api = playwright.request.new_context(base_url="https://sqatools.in")
    response = api.get("/automation-practice-page/")
    assert response.ok
    api.dispose()
  • playwright.devices contains the device descriptors used by --device. You can pass one to new_context(**playwright.devices["Pixel 7"]).
  • playwright.request creates API clients without a browser. Module 9 covers API testing in detail.

๐Ÿ“‹ All Built-in Fixtures at a Glance

FixtureScopeTypical use
pagefunctionAlmost every UI test
contextfunctionCookies, permissions, routes, a second page in the same session
new_contextfunctionSeveral independent users or per-test context options
browsersessionContexts without the default settings
browser_type, launch_browsersessionLaunching another browser yourself
playwrightsessionDevices, API request contexts
browser_context_argssessionOverride to change context settings (Lesson 6)
browser_type_launch_argssessionOverride to change launch options (headless, args, channel)
connect_optionssessionOverride to connect to a remote browser server
browser_name, browser_channel, devicesessionWhere the test runs
is_chromium, is_firefox, is_webkitsessionBrowser-specific branches
output_pathfunctionThis test’s artifact folder
base_urlsessionThe --base-url value

โ“ Frequently Asked Questions

๐Ÿค” Is the browser restarted for every test?

No. The browser fixture is session-scoped, so one browser is launched per browser type for the whole run (per worker when you use pytest-xdist). Each test gets a fresh context instead, which is fast and fully isolated.

๐Ÿค” Can I make the page fixture shared between tests to save time?

You could write your own session-scoped page fixture, but tests would then share cookies and state and depend on each other’s order. Keep the per-test context; to save login time, reuse a saved login state instead (Module 8).

๐Ÿค” What is the difference between context and new_context?

context is one ready-made context for the test. new_context is a function that creates as many extra contexts as you need, with optional per-context settings. All of them are closed automatically after the test.

๐Ÿค” Why do all my test names end with [chromium]?

pytest-playwright parametrizes browser tests by browser_name. The suffix shows which browser ran that copy of the test, which matters when you pass several --browser options.


๐Ÿ Summary

  • page sits on a chain: playwright โ†’ browser_type โ†’ browser (session) โ†’ new_context โ†’ context โ†’ page (per test).
  • One shared browser makes runs fast; a fresh context per test keeps them isolated.
  • --setup-show reveals which fixtures run and in which scope.
  • new_context creates extra isolated contexts with optional settings and closes them for you.
  • browser.new_context() skips the project settings and must be closed by you.
  • browser_name, is_*, browser_channel, device, output_path and base_url tell a test where and how it runs.

Next, you will write your own fixtures on top of these: page objects, logged-in pages, test data with cleanup and fixture factories.

๐ŸŽฌ Watch the Playwright tutorial series on YouTube!

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

Leave a Comment