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.
๐ 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:
| Fixture | Scope | What it gives you |
|---|---|---|
playwright | session | The started Playwright object (sync_playwright().start()) |
browser_type | session | playwright.chromium, .firefox or .webkit, chosen by --browser |
browser | session | One launched browser, shared by all tests of that browser |
new_context | function | A function that creates browser contexts and closes them after the test |
context | function | A fresh BrowserContext for this test, made with new_context() |
page | function | A 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_argsand others) are set up first. Fixtures whose names start with_pware the plugin’s internal helpers; you never use them directly. browser_nameis 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,contextandpageare created just for this test, then yourpracticefixture runs on top ofpage. - 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"])
SEENis 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.localStorageis read and written withpage.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, isolatedBrowserContext. - Options passed to the call, such as
localeorviewport, are added to the configuredbrowser_context_argsfor 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/finallymakes sure the context is closed even if the test fails.- Prefer
new_contextin tests. Usebrowser.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:
| Fixture | Value |
|---|---|
browser_name | "chromium", "firefox" or "webkit" |
is_chromium, is_firefox, is_webkit | True or False |
browser_channel | The --browser-channel value, such as "chrome", or None |
device | The --device value, such as "iPhone 15", or None |
output_path | This 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, throughbrowser_context_args.- The iPhone descriptor names WebKit as its default browser, but
--browserdecides 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_pathis 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.devicescontains the device descriptors used by--device. You can pass one tonew_context(**playwright.devices["Pixel 7"]).playwright.requestcreates API clients without a browser. Module 9 covers API testing in detail.
๐ All Built-in Fixtures at a Glance
| Fixture | Scope | Typical use |
|---|---|---|
page | function | Almost every UI test |
context | function | Cookies, permissions, routes, a second page in the same session |
new_context | function | Several independent users or per-test context options |
browser | session | Contexts without the default settings |
browser_type, launch_browser | session | Launching another browser yourself |
playwright | session | Devices, API request contexts |
browser_context_args | session | Override to change context settings (Lesson 6) |
browser_type_launch_args | session | Override to change launch options (headless, args, channel) |
connect_options | session | Override to connect to a remote browser server |
browser_name, browser_channel, device | session | Where the test runs |
is_chromium, is_firefox, is_webkit | session | Browser-specific branches |
output_path | function | This test’s artifact folder |
base_url | session | The --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
pagesits 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-showreveals which fixtures run and in which scope.new_contextcreates 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_pathandbase_urltell 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