Playwright with Python, Java & .NET
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Playwright is one implementation with four language bindings. The browser automation, the locators, auto-waiting and web-first assertions behave the same in TypeScript, Python, Java and .NET, but the test runner around them does not: Node.js has Playwright Test, which bundles a runner, a config file, projects, sharding and reporters, while the other three languages plug Playwright into the runner their ecosystem already uses (pytest, JUnit or TestNG, NUnit, MSTest or xUnit). This page writes the same Bookshelf scenario in all three languages, explains the library mode versus test-runner mode split, and lists what you give up or do differently outside Node.js.
The pages of this section were written against Playwright 1.64.1 for Node.js and checked on 25 October 2026. The Python,
Java and .NET examples on this page were run with the 1.63.0 bindings (Python playwright 1.63.0 with pytest-playwright
0.10.0, Maven com.microsoft.playwright:playwright 1.63.0, NuGet Microsoft.Playwright.* 1.63.0), because the language
documentation at the check date still lists 1.63 as its newest release. Newer bindings were already published at that
date: PyPI playwright 1.64.1, Maven Central 1.64.0 and NuGet 1.64.1. Everything below applies to them as well, but it
was not re-run on 1.64. Every example ran against the
Bookshelf application on a local port, with the reader’s
password read from the BOOKSHELF_READER_PASSWORD environment variable. The page-object and fixture ideas used here are
explained in Test Design & Page Objects; the Node.js
equivalent of every example is in Playwright: Getting Started.
The scenario every language runs
Two tests, the same in every binding:
-
Search finds a book. Open
/books, typequietinto the search box, press Search, and expect exactly one book card whose heading is "The Quiet Compiler". -
A reader adds a review. Sign in as
reader(password fromBOOKSHELF_READER_PASSWORD), expect to land on/account, open/books/3, choose a rating of 4, write a review, submit it, and expect the new review row to reachdata-state="saved".
Each test starts from known data by calling POST /api/test/reset through Playwright’s own HTTP client before the test
runs, so no test depends on what the previous one left behind. In the reviews test this matters: without the reset, a
second run finds two matching review rows (the saved one and the optimistic pending one) and the locator violates
strict mode.
Packages, runners and what each one gives you
| Language | Packages | Recommended test runner | What the runner integration provides |
|---|---|---|---|
Python |
|
pytest |
|
Java |
|
Any: JUnit 5 and TestNG are documented; JUnit also has the experimental |
Plain library objects you create yourself, or (JUnit only) injected |
.NET |
|
NUnit, MSTest and xUnit, through base classes |
|
Node.js (for comparison) |
|
Playwright Test |
Everything: config file, projects, workers, sharding, retries, reporters, UI Mode, snapshots. |
Source: Playwright — Supported languages
The official language overview states that all core features for automating the browser are supported in all languages, while the testing ecosystem integration differs, and recommends choosing the language by your team’s experience and project constraints, with the runner recommended for that language.
Library mode and test-runner mode
The same split exists in Node.js and is the key to reading the other languages' documentation. Library mode means you call the automation API directly: you pick a browser type, launch it, create a context and a page, run your steps and close everything yourself. Test-runner mode means a runner owns that lifecycle and hands your test a ready page.
| Concern | Library (any language) | Test runner |
|---|---|---|
Installation |
|
Runner package ( |
Browser, context, page |
You launch the browser, call |
The runner creates an isolated context and page for each test, lazily, and applies the options from flags, configuration or overridable methods |
Assertions |
The language’s own assertions, or the retrying web-first helpers that the Python, Java and .NET libraries also ship
( |
The same retrying web-first assertions, plus the runner’s assertion reporting |
Timeouts |
Actions default to 30 s |
Actions follow the runner’s settings and every test also has its own timeout (Playwright Test, 30 s by default) |
Cleanup |
You close contexts and browsers (in Python and Java with context managers or |
The runner closes the fixtures |
Running |
You run a script ( |
You run the runner ( |
Source: Playwright — Library
Use test-runner mode for end-to-end testing. Library mode is the right tool for scripts that are not tests: scraping,
generating screenshots or PDFs, warming a cache, or a custom harness. The Python examples below include a library-mode
script in both the sync and the async flavor; the Java TracingTest and the Java plain-library test show that in Java
the line between the two is thin, because the Java documentation recommends building your own @BeforeAll and
@BeforeEach fixtures around the library when you do not use the experimental JUnit integration.
Python with pytest-playwright
Installing
Python needs a supported interpreter. The Python documentation lists "Python 3.8 or higher", but the PyPI metadata of the 1.6x releases requires Python 3.10 or later, and the macOS system Python 3.9 of the machine used here was too old. The runs below used Python 3.13 in a virtual environment.
python3 -m venv .venv
.venv/bin/pip install pytest-playwright==0.10.0 playwright==1.63.0
.venv/bin/playwright install chromium webkit # download the browsers
.venv/bin/playwright --version
# Version 1.63.0
Source: Playwright Python — Installation
pip install pytest-playwright pulls in playwright and python-slugify; the plugin then provides the
--base-url option itself in the 0.10 release. playwright install with no arguments downloads Chromium, Firefox and
WebKit; name the engines you need to save time and disk space, add --with-deps on a fresh Linux image to install the
operating-system libraries as well, and use --dry-run to print what would be downloaded and where. Browsers are cached
per user (~/Library/Caches/ms-playwright on macOS) and shared between projects that use the same Playwright version.
Environment variables change the behaviour: PLAYWRIGHT_BROWSERS_PATH moves the cache, PLAYWRIGHT_DOWNLOAD_HOST
downloads from an internal mirror, HTTPS_PROXY sends downloads through a proxy and PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD
skips the download.
$ .venv/bin/playwright install --dry-run chromium | head -4
Chrome for Testing 153.0.8010.12 (playwright chromium v1243)
Install location: /Users/me/Library/Caches/ms-playwright/chromium-1243
Download url: https://cdn.playwright.dev/builds/cft/153.0.8010.12/mac-arm64/chrome-mac-arm64.zip
Source: Playwright Python — Browsers
The Bookshelf test
The plugin needs no configuration to run: a test function that asks for the page fixture gets a new page in a new
context. The base URL is the only setting the tests need, and it goes in pytest.ini so that a plain pytest works
(the command line can override it with --base-url).
# pytest.ini
[pytest]
addopts = --base-url=http://localhost:3920 --output=test-results
testpaths = tests
# tests/conftest.py
import pytest
from playwright.sync_api import APIRequestContext, Playwright, expect
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
# Extends the options the plugin builds from the CLI flags (--device, --base-url, ...).
return {**browser_context_args, "locale": "en-GB"}
@pytest.fixture(autouse=True)
def reset_data(playwright: Playwright, base_url: str):
# Arrange through the HTTP API instead of the UI, as in the other examples.
request: APIRequestContext = playwright.request.new_context(base_url=base_url)
request.post("/api/test/reset")
request.dispose()
def pytest_configure():
expect.set_options(timeout=5_000)
# tests/test_bookshelf.py
import os
import re
import pytest
from playwright.sync_api import Page, expect
READER_PASSWORD = os.environ["BOOKSHELF_READER_PASSWORD"]
def test_search_finds_a_book(page: Page):
page.goto("/books")
page.get_by_test_id("search-input").fill("quiet")
page.get_by_role("button", name="Search").click()
expect(page.get_by_test_id("book-card")).to_have_count(1)
expect(page.get_by_role("heading", name="The Quiet Compiler")).to_be_visible()
def test_reader_adds_a_review(page: Page):
page.goto("/signin")
page.get_by_label("Username").fill("reader")
page.get_by_label("Password").fill(READER_PASSWORD)
page.get_by_test_id("signin-submit").click()
expect(page).to_have_url(re.compile(r"/account$"))
page.goto("/books/3")
page.get_by_label("Rating").select_option("4")
page.get_by_label("Your review").fill("A lovely, detailed field guide.")
page.get_by_role("button", name="Submit review").click()
review = page.get_by_test_id("review").filter(has_text="lovely")
expect(review).to_have_attribute("data-state", "saved")
@pytest.mark.browser_context_args(timezone_id="Europe/Berlin", locale="de-DE")
def test_context_args_marker(page: Page):
page.goto("/")
assert page.evaluate("navigator.language") == "de-DE"
assert page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone") == "Europe/Berlin"
def test_two_users_with_new_context(page: Page, new_context):
other = new_context()
other_page = other.new_page()
page.goto("/signin")
other_page.goto("/signin")
assert page.context is not other_page.context
$ export BOOKSHELF_READER_PASSWORD=... # the reader's password; never commit it
$ .venv/bin/pytest -v
collected 4 items
tests/test_bookshelf.py::test_search_finds_a_book[chromium] PASSED [ 25%]
tests/test_bookshelf.py::test_reader_adds_a_review[chromium] PASSED [ 50%]
tests/test_bookshelf.py::test_context_args_marker[chromium] PASSED [ 75%]
tests/test_bookshelf.py::test_two_users_with_new_context[chromium] PASSED [100%]
============================== 4 passed in 1.06s ===============================
Notes on the code:
-
The
[chromium]suffix is added by the plugin: the browser is a test parameter, so one test function becomes one test per--browservalue. -
browser_context_argsis a session-scoped fixture that returns the keyword arguments ofbrowser.new_context(). Override it inconftest.pyand extend the dictionary you receive ({**browser_context_args, ...}) so the options the plugin derived from the command line, such as the base URL or--device, survive. -
The
browser_context_argsmarker overrides the same options for a single test. The official snippet for this marker asserts the time zone againstnavigator.userAgent; the test above checksnavigator.languageand the resolved time zone instead. -
new_contextis a factory fixture for extra contexts (a second user, for example). Contexts it creates are closed by the plugin at the end of the test. -
playwright.request.new_context(...)is the API-testing client, used here to reset the data without a browser. -
expect.set_options(timeout=...)sets the default timeout of the retrying assertions for the whole run (5 s is the default). Hooking it intopytest_configurekeeps it out of the individual tests.
Command line flags
The plugin adds these options to pytest. They configure the built-in browser, context and page fixtures only:
a browser or context you create yourself with browser.new_context() is not affected.
| Flag | Effect | Default |
|---|---|---|
|
Engine to run: |
|
|
Show the browser window. |
headless |
|
Branded browser to use, for example |
none (bundled engine) |
|
Slow every Playwright operation down by this many milliseconds. |
|
|
Emulate a device from Playwright’s device list, for example |
none |
|
Directory for artifacts produced by tests. |
|
|
Record a trace per test: |
|
|
Record a video per test: |
|
|
Take a screenshot after each test: |
|
|
Capture the full page instead of the viewport; needs |
off |
|
Base URL of the application; relative |
none |
|
Appears in |
off |
Putting flags in addopts of pytest.ini applies them to every run, as the documentation shows
(addopts = --headed --browser firefox). The following run used Chromium and WebKit together with all artifacts on:
$ .venv/bin/pytest --browser chromium --browser webkit --tracing on --video on --screenshot on -v
...
tests/test_bookshelf.py::test_search_finds_a_book[chromium] PASSED [ 12%]
...
tests/test_bookshelf.py::test_two_users_with_new_context[webkit] PASSED [100%]
============================== 8 passed in 5.95s ===============================
$ find test-results/tests-test-bookshelf-py-test-reader-adds-a-review-chromium -type f
.../video.webm
.../test-finished-1.png
.../trace-1.zip
.../trace-2.zip
With --tracing on the plugin writes one numbered trace per browser context the test used (the second one here comes
from the API-request context of the reset fixture). The failure-only modes keep a single file for a failing test. This
run used the deliberately failing test expect(page.get_by_test_id("book-card")).to_have_count(7, timeout=1000):
$ .venv/bin/pytest failing --tracing retain-on-failure --screenshot only-on-failure \
--video retain-on-failure --slowmo 100
...
E AssertionError: Locator expected to have count '7'
E Actual value: 6
E Call log:
E - Expect "to_have_count" get_by_test_id("book-card") with timeout 1000ms
E - waiting for get_by_test_id("book-card")
E 11 × locator resolved to 6 elements
E - unexpected value "6"
...
FAILED failing/test_fail.py::test_wrong_count[chromium]
$ find test-results -type f
test-results/failing-test-fail-py-test-wrong-count-chromium/test-failed-1.png
test-results/failing-test-fail-py-test-wrong-count-chromium/video.webm
test-results/failing-test-fail-py-test-wrong-count-chromium/trace.zip
Source: Playwright Python — Trace viewer
Open a trace with playwright show-trace test-results/.../trace.zip or upload it to the hosted viewer at
https://trace.playwright.dev. The viewer itself is explained in
Debugging, Tracing & Codegen. The command was not run
here, because it opens a window. --headed was not run for the same reason; it behaves as in the other languages.
Fixtures
| Fixture | Scope | What you get |
|---|---|---|
|
function |
A new page in its own new context. The workhorse of almost every test. |
|
function |
A new browser context; the |
|
function |
A factory returning more contexts, with the same arguments as |
|
session |
The |
|
session |
The |
|
session |
The engine and channel as strings. |
|
session |
Booleans for conditional logic in a test or fixture. |
|
session |
Override to change the arguments of |
|
session |
Override to change the arguments of |
|
session |
Return the options of |
Two markers help with browser matrices: @pytest.mark.skip_browser("firefox") skips a test on one engine and
@pytest.mark.only_browser("chromium") restricts it to one. Plugin-supplied --base-url comes from the base_url
fixture, so other fixtures (like reset_data above) can depend on it.
Parallel runs, unittest and debugging
Tests run one after the other in a single process by default. Install pytest-xdist and pass --numprocesses (-n)
to run them in several processes; the Python documentation suggests a number between 2 and the number of CPUs, and half
the logical cores for the running-tests guide. Each worker launches its own browser:
$ .venv/bin/pip install pytest-xdist
$ .venv/bin/pytest tests -n 2
2 workers [4 items]
.... [100%]
============================== 4 passed in 1.51s ===============================
The data-reset fixture above runs once per test in every worker, so two workers that reset the same Bookshelf can overwrite each other’s data. For parallel runs, give each worker its own data (for example with different books, users or a per-worker server) rather than one global reset; the isolation techniques are covered in Authentication & Isolation and Parallelism, Sharding & Retries.
Further points from the plugin reference, documented but not run here: unittest.TestCase classes can use the fixtures
through an autouse pytest fixture that stores the page on the instance, with the limitation that only one browser can
be specified; --browser-channel chrome runs against an installed Chrome; breakpoint() in a test opens a pdb prompt;
and PWDEBUG=1 pytest -s -k test_name opens the Playwright Inspector.
Async tests
The default plugin uses the sync API (playwright.sync_api), which looks like ordinary blocking code. If your tests
already live in an asyncio application, use the async API (playwright.async_api), install pytest-playwright-asyncio
(which replaces the sync fixtures with async ones) and pytest-asyncio 0.26 or later, and set the session loop scope. The
sync API cannot run inside a running event loop, so keep the two flavors in separate test suites (the plugin documentation
shows pytest-playwright-asyncio as the way to get async fixtures). The async variant was run in its own virtual environment (pytest-asyncio 1.4.0):
# async_tests/pytest.ini
[pytest]
asyncio_default_test_loop_scope = session
asyncio_default_fixture_loop_scope = session
addopts = --base-url=http://localhost:3920 --output=test-results
# async_tests/conftest.py
import pytest_asyncio
from playwright.async_api import Playwright
@pytest_asyncio.fixture(autouse=True, loop_scope="session")
async def reset_data(playwright: Playwright, base_url: str):
request = await playwright.request.new_context(base_url=base_url)
await request.post("/api/test/reset")
await request.dispose()
# async_tests/test_async.py
import os
import re
import pytest
from playwright.async_api import Page, expect
@pytest.mark.asyncio(loop_scope="session")
async def test_reader_adds_a_review(page: Page):
await page.goto("/signin")
await page.get_by_label("Username").fill("reader")
await page.get_by_label("Password").fill(os.environ["BOOKSHELF_READER_PASSWORD"])
await page.get_by_test_id("signin-submit").click()
await expect(page).to_have_url(re.compile(r"/account$"))
await page.goto("/books/3")
await page.get_by_label("Rating").select_option("4")
await page.get_by_label("Your review").fill("A lovely, detailed field guide.")
await page.get_by_role("button", name="Submit review").click()
await expect(page.get_by_test_id("review").filter(has_text="lovely")).to_have_attribute("data-state", "saved")
$ ../.venv-async/bin/pytest -v
test_async.py::test_reader_adds_a_review[chromium] PASSED [100%]
============================== 1 passed in 0.64s ===============================
Library mode in Python
Without pytest, use the sync context manager for scripts, or the async one inside an event loop. Both were run, and both printed the same line:
# library_sync.py
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(base_url="http://localhost:3920")
page = context.new_page()
page.goto("/books")
page.get_by_test_id("search-input").fill("quiet")
page.get_by_role("button", name="Search").click()
page.wait_for_selector("[data-testid=book-card]")
print(page.get_by_test_id("book-card").count(), "book found:", page.get_by_test_id("book-title").first.text_content())
context.close()
browser.close()
# library_async.py
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(base_url="http://localhost:3920")
await page.goto("/books")
await page.get_by_test_id("search-input").fill("quiet")
await page.get_by_role("button", name="Search").click()
await page.get_by_test_id("book-card").first.wait_for()
print(await page.get_by_test_id("book-card").count(), "book found:", await page.get_by_test_id("book-title").first.text_content())
await browser.close()
asyncio.run(main())
1 book found: The Quiet Compiler
In library mode you control tracing yourself. Since 1.63 tracing.start() also accepts aria_snapshots and
screen_snapshots (see the section on recent changes below). This script ran and produced a 238 KB trace:
context.tracing.start(snapshots=True, aria_snapshots=True, screen_snapshots=True, sources=True)
page = context.new_page()
page.goto("/books")
page.get_by_test_id("book-card").first.click()
context.tracing.stop(path="test-results/library-trace.zip")
Source: Playwright Python — Trace viewer
Java
Installing
Playwright for Java is a set of Maven modules; adding the playwright artifact to the project is enough, because the
first Playwright.create() call extracts the driver and downloads the browsers it does not find (the Java
documentation: "Running it downloads the Playwright package and installs browser binaries for Chromium, Firefox and
WebKit"). The documented requirement is Java 8 or later; the runs below used JDK 17, JUnit Jupiter 5.14.4 and Surefire
3.5.4.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>bookshelf-playwright</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.63.0</playwright.version>
</properties>
<dependencies>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${playwright.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>
Source: Playwright Java — Installation
To install the browsers (and system dependencies) explicitly, for example in a CI image build step, run the bundled CLI
class. With the documented dependency in the default (compile) scope the command is the one from the official guide.
Because the pom above uses test scope, Maven’s exec:java also needs -D exec.classpathScope=test; without it the
run fails because the CLI class is not on the classpath:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"
# test-scoped dependency:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.classpathScope=test \
-D exec.args="install --with-deps chromium"
# the same CLI prints what it would do:
mvn -q exec:java -D exec.mainClass=com.microsoft.playwright.CLI -D exec.classpathScope=test \
-D exec.args="install --dry-run chromium"
Source: Playwright Java — Browsers
The install --dry-run chromium form was run and printed the same download plan as the Python CLI. The other two
commands were not run, because the browsers were already installed. With Gradle, the documentation adds a JavaExec
task for the CLI (./gradlew playwright --args="help"):
// build.gradle
plugins {
application
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.microsoft.playwright:playwright:1.63.0'
}
application {
mainClass = 'org.example.App'
}
// Usage: ./gradlew playwright --args="help"
task playwright(type: JavaExec) {
classpath sourceSets.test.runtimeClasspath
mainClass = 'com.microsoft.playwright.CLI'
}
test {
useJUnitPlatform()
}
Source: Playwright Java — Test Runners
The Gradle build was not run here (the documented build file is shown as published). Environment variables work as in the
other languages: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 stops the automatic download, and
Playwright.create(new Playwright.CreateOptions().setEnv(...)) passes variables to the driver for a single instance.
Plain library with JUnit 5 lifecycle methods
The Java documentation does not ship a runner of its own; the recommended structure for JUnit 5 is one Playwright and
one Browser for the class, created in @BeforeAll and closed in @AfterAll, and one BrowserContext with its Page per
test, created in @BeforeEach and closed in @AfterEach. This is the Bookshelf test in that shape:
package demo;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
import java.util.regex.Pattern;
import org.junit.jupiter.api.*;
class BookshelfPlainTest {
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:3000");
static final String READER_PASSWORD = System.getenv("BOOKSHELF_READER_PASSWORD");
// Shared by every test in the class.
static Playwright playwright;
static Browser browser;
// One new instance per test.
BrowserContext context;
Page page;
@BeforeAll
static void launchBrowser() {
playwright = Playwright.create();
browser = playwright.chromium().launch();
APIRequestContext request = playwright.request().newContext(new APIRequest.NewContextOptions().setBaseURL(BASE_URL));
request.post("/api/test/reset");
request.dispose();
}
@AfterAll
static void closeBrowser() {
playwright.close();
}
@BeforeEach
void createContextAndPage() {
context = browser.newContext(new Browser.NewContextOptions().setBaseURL(BASE_URL));
page = context.newPage();
}
@AfterEach
void closeContext() {
context.close();
}
@Test
void searchFindsABook() {
page.navigate("/books");
page.getByTestId("search-input").fill("quiet");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Search")).click();
assertThat(page.getByTestId("book-card")).hasCount(1);
assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("The Quiet Compiler"))).isVisible();
}
@Test
void readerAddsAReview() {
page.navigate("/signin");
page.getByLabel("Username").fill("reader");
page.getByLabel("Password").fill(READER_PASSWORD);
page.getByTestId("signin-submit").click();
assertThat(page).hasURL(Pattern.compile("/account$"));
page.navigate("/books/3");
page.getByLabel("Rating").selectOption("4");
page.getByLabel("Your review").fill("A lovely, detailed field guide.");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit review")).click();
Locator review = page.getByTestId("review").filter(new Locator.FilterOptions().setHasText("lovely"));
assertThat(review).hasAttribute("data-state", "saved");
}
}
Source: Playwright Java — Test Runners
The Java API has no default parameters, so optional arguments are builder objects: Page.GetByRoleOptions,
Locator.FilterOptions, Browser.NewContextOptions. Enums are upper case (AriaRole.BUTTON). Assertions come from
PlaywrightAssertions.assertThat(...), whose overloads cover Page, Locator and APIResponse; they retry until the
condition holds or the timeout (5 s by default) expires, and a failure raises JUnit’s AssertionFailedError with the
call log. PlaywrightAssertions.setDefaultAssertionTimeout(...) changes the default for the process. The class was run
together with the one below:
$ export BOOKSHELF_READER_PASSWORD=... BASE_URL=http://localhost:3920
$ mvn -o test
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.059 s -- in demo.BookshelfPlainTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.030 s -- in demo.BookshelfJUnitTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.425 s -- in demo.TracingTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
A failing assertion (hasCount(7, new LocatorAssertions.HasCountOptions().setTimeout(1000)) on a page with six cards)
produced:
org.opentest4j.AssertionFailedError:
Locator expected to have count
Expected: 7
Received: 6
Call log:
- Assert "hasCount" getByTestId("book-card") with timeout 1000ms
- waiting for getByTestId("book-card")
11 × locator resolved to 6 elements
- unexpected value "6"
Source: Playwright Java — Assertions
JUnit 5 with the experimental @UsePlaywright fixtures
Since Playwright for Java 1.42 an experimental JUnit extension injects the objects. Annotate the class with
@UsePlaywright and declare Page, BrowserContext, Browser, Playwright or APIRequestContext as test-method
parameters. The page and browserContext fixtures are isolated per test, the browser is shared across tests, and
the playwright instance is shared between tests that run on the same thread. To customize them, implement
OptionsFactory and pass the class to the annotation.
package demo;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import com.microsoft.playwright.*;
import com.microsoft.playwright.junit.Options;
import com.microsoft.playwright.junit.OptionsFactory;
import com.microsoft.playwright.junit.UsePlaywright;
import com.microsoft.playwright.options.AriaRole;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
@UsePlaywright(BookshelfJUnitTest.BookshelfOptions.class)
class BookshelfJUnitTest {
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:3000");
public static class BookshelfOptions implements OptionsFactory {
@Override
public Options getOptions() {
return new Options()
.setHeadless(true)
.setBaseUrl(BASE_URL)
.setApiRequestOptions(new APIRequest.NewContextOptions().setBaseURL(BASE_URL))
.setTestIdAttribute("data-testid")
.setTrace(Options.Trace.RETAIN_ON_FAILURE)
.setOutputDir(Paths.get("target/playwright-output"));
}
}
@Test
void readerAddsAReview(Page page, APIRequestContext request) {
request.post("/api/test/reset");
page.navigate("/signin");
page.getByLabel("Username").fill("reader");
page.getByLabel("Password").fill(System.getenv("BOOKSHELF_READER_PASSWORD"));
page.getByTestId("signin-submit").click();
assertThat(page).hasURL(Pattern.compile("/account$"));
page.navigate("/books/3");
page.getByLabel("Rating").selectOption("4");
page.getByLabel("Your review").fill("A lovely, detailed field guide.");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit review")).click();
assertThat(page.getByTestId("review").filter(new Locator.FilterOptions().setHasText("lovely")))
.hasAttribute("data-state", "saved");
}
@Test
void searchFindsABook(Page page) {
page.navigate("/books");
page.getByTestId("search-input").fill("quiet");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Search")).click();
assertThat(page.getByTestId("book-card")).hasCount(1);
}
}
The members of Options in the 1.63.0 jar, which were listed with javap:
| Setter | Effect |
|---|---|
|
Run headed or headless, pick a branded channel, pick |
|
Base URL of the injected browser contexts (and so of |
|
Options for the injected |
|
Full |
|
Emulate a device, ignore certificate errors, change the attribute used by |
|
Options for |
|
|
Three differences from the published documentation showed up when running this against 1.63.0:
-
The documentation example calls
setContextOption(...). The class hassetContextOptions(...)(plural), so the documented snippet does not compile as written. -
setBaseUrl(...)applies to the injected browser contexts only. The injectedAPIRequestContexthad no base URL untilsetApiRequestOptions(...)was added;request.post("/api/test/reset")otherwise failed with aTypeError: Invalid URLfrom the driver. -
With
Options.Trace.RETAIN_ON_FAILUREa passing test leaves nothing behind; a failing one lefttarget/playwright-output/demo.BookshelfJUnitTest.readerAddsAReview-chromium/trace.zip.
The extension is still labelled experimental in the documentation at the check date, as it has been since its introduction in 1.42, so its API may change; the plain-library structure above does not have that risk.
Tracing in Java
Outside the experimental extension, tracing is explicit. This test ran:
@Test
void recordsATrace() {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setBaseURL(BASE_URL));
// Tracing is explicit in library mode: start before the actions, stop with a path to keep it.
context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true).setSources(true));
Page page = context.newPage();
page.navigate("/books");
assertThat(page.getByTestId("book-card")).hasCount(6);
context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get("target/traces/books.zip")));
browser.close();
}
}
Source: Playwright Java — Trace viewer
Open the trace with the bundled CLI:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace target/traces/books.zip".
That command opens a window and was not run.
Running tests in parallel with JUnit 5
JUnit runs tests sequentially on one thread by default. Playwright objects are not thread-safe, so the Java
documentation recommends one Playwright instance per thread, used only on that thread. With @UsePlaywright the
extension does this for you. With the plain structure, annotate the base class
@TestInstance(TestInstance.Lifecycle.PER_CLASS) and keep Playwright and Browser in instance fields so that every
test class has its own copy. Then let classes run concurrently but keep the methods of one class sequential:
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=0.5
Source: Playwright Java — Test Runners
This configuration was not run (the Bookshelf data-reset caveat from the Python section applies equally). TestNG is
documented with the same structure using @BeforeClass, @AfterClass, @BeforeMethod and @AfterMethod; it was not
run here.
Headed mode and debugging in Java
Pass new BrowserType.LaunchOptions().setHeadless(false) to launch(...) (or setHeadless(false) in the JUnit
Options), optionally with .setSlowMo(50). PWDEBUG=1 mvn test opens the Inspector, and PLAYWRIGHT_JAVA_SRC lets
the Inspector show the Java sources. Neither was run, because they open windows.
.NET
Installing
Playwright for .NET is a .NET Standard 2.0 library; the documentation recommends .NET 8, and the runs here used the .NET 10 SDK. Create a test project, add the package for your runner, build once so that the installation script is generated, then install the browsers:
dotnet new nunit -n PlaywrightTests # or: mstest, xunit, xunit3
cd PlaywrightTests
dotnet add package Microsoft.Playwright.NUnit # or MSTest, MSTest.v4, Xunit, Xunit.v3
dotnet build
pwsh bin/Debug/net8.0/playwright.ps1 install
Source: Playwright .NET — Installation
Adjust net8.0 to the target framework of your project (net10.0 here). The packages and their namespaces:
| Runner | NuGet package | Namespace of PageTest |
|---|---|---|
NUnit |
|
|
MSTest 3 |
|
|
MSTest 4 |
|
|
xUnit 2.x |
|
|
xUnit v3 |
|
|
Source: Playwright .NET — Test Runners
The MSTest v4 and xUnit v3 packages were added in 1.55. Two things were learned by running all four runners with the 1.63.0 packages:
-
Microsoft.Playwright.MSTest1.63.0 together with the currentMSTest4.0.2 meta-package built, butdotnet testdiscovered no tests. WithMSTest3.11.1 it ran normally, andMicrosoft.Playwright.MSTest.v4works withMSTest4.0.2. Match the Playwright package to the major version of MSTest you use. -
A project cannot be named like one of its dependencies: a project called
Nunitfailed to restore with a dependency cycle against theNUnitpackage (NU1108). The projects here are calledNUnitTests,MSTestTestsand so on.
If PowerShell is not available
playwright.ps1 is a PowerShell script, and the documentation says to install PowerShell if pwsh is missing. The
script is a thin wrapper: the build also copies the Playwright driver, a Node.js runtime and the package into
bin/Debug/net10.0/.playwright, so the same installation can be done by calling the bundled Node.js on the bundled
cli.js. This is what was done on a macOS machine without pwsh:
$ bin/Debug/net10.0/.playwright/node/darwin-arm64/node \
bin/Debug/net10.0/.playwright/package/cli.js install chromium
$ bin/Debug/net10.0/.playwright/node/darwin-arm64/node \
bin/Debug/net10.0/.playwright/package/cli.js install --dry-run chromium | head -3
Chrome for Testing 153.0.8010.12 (playwright chromium v1243)
Install location: /Users/me/Library/Caches/ms-playwright/chromium-1243
Download url: https://cdn.playwright.dev/builds/cft/153.0.8010.12/mac-arm64/chrome-mac-arm64.zip
Source: Playwright .NET — Browsers
The folder under .playwright/node is named after the operating system and CPU of the machine (darwin-arm64 here), so
adapt it on Linux and Windows. Every argument accepted by playwright.ps1 (install --with-deps, codegen,
show-trace) can be passed to cli.js the same way. This is not an officially documented procedure, but it is the
mechanism the script uses.
The Bookshelf test with NUnit
The base class gives every test a fresh page in its own browser context; override ContextOptions() to set the base URL
and other context options. The project file is the generated NUnit one with the package set to
Microsoft.Playwright.NUnit 1.63.0.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.63.0" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
</Project>
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
namespace Bookshelf.Tests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class BookshelfTests : PageTest
{
private static readonly string BaseUrl =
Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost:3000";
private static readonly string ReaderPassword =
Environment.GetEnvironmentVariable("BOOKSHELF_READER_PASSWORD")
?? throw new InvalidOperationException("Set BOOKSHELF_READER_PASSWORD");
public override BrowserNewContextOptions ContextOptions() => new()
{
BaseURL = BaseUrl,
Locale = "en-GB",
};
[SetUp]
public async Task ResetData()
{
// Arrange through the HTTP API, not the UI.
var request = await Playwright.APIRequest.NewContextAsync(new() { BaseURL = BaseUrl });
await request.PostAsync("/api/test/reset");
await request.DisposeAsync();
}
[Test]
public async Task SearchFindsABook()
{
await Page.GotoAsync("/books");
await Page.GetByTestId("search-input").FillAsync("quiet");
await Page.GetByRole(AriaRole.Button, new() { Name = "Search" }).ClickAsync();
await Expect(Page.GetByTestId("book-card")).ToHaveCountAsync(1);
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "The Quiet Compiler" })).ToBeVisibleAsync();
}
[Test]
public async Task ReaderAddsAReview()
{
await Page.GotoAsync("/signin");
await Page.GetByLabel("Username").FillAsync("reader");
await Page.GetByLabel("Password").FillAsync(ReaderPassword);
await Page.GetByTestId("signin-submit").ClickAsync();
await Expect(Page).ToHaveURLAsync(new Regex("/account$"));
await Page.GotoAsync("/books/3");
await Page.GetByLabel("Rating").SelectOptionAsync("4");
await Page.GetByLabel("Your review").FillAsync("A lovely, detailed field guide.");
await Page.GetByRole(AriaRole.Button, new() { Name = "Submit review" }).ClickAsync();
var review = Page.GetByTestId("review").Filter(new() { HasText = "lovely" });
await Expect(review).ToHaveAttributeAsync("data-state", "saved");
}
}
Source: Playwright .NET — Installation
$ export BOOKSHELF_READER_PASSWORD=... BASE_URL=http://localhost:3920
$ dotnet test
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, Duration: 2 s - NUnitTests.dll (net10.0)
Every I/O call is async and carries the Async suffix; options are passed as target-typed new() { ... }
objects; locator methods such as GetByRole take the AriaRole enum. Expect(...) is a method of the base class and
returns the assertion object (Expect(Page), Expect(locator)); assertion methods end in Async. The base classes are:
| Base class | What each test gets |
|---|---|
|
A fresh |
|
A fresh |
|
A |
|
A |
Source: Playwright .NET — Test Runners
MSTest and xUnit
The scenario is the same. Only the attributes and the using for the base class change, so the projects that were run
kept the test bodies in shared private helper methods and put the runner attributes on thin test methods. The
differences:
| Runner | using for the base class |
Class attribute | Test attribute | Result |
|---|---|---|---|---|
NUnit |
|
|
|
2 passed |
MSTest 3 ( |
|
|
|
2 passed |
MSTest 4 ( |
|
|
|
2 passed |
xUnit 2.9.3 |
|
none |
|
2 passed |
xUnit v3 ( |
|
none |
|
2 passed |
Source: Playwright .NET — Installation
An xUnit v3 test project builds an executable, so its project file needs <OutputType>Exe</OutputType> and the
xunit.v3 package:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.Playwright.Xunit.v3" Version="1.63.0" />
<PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
Source: Playwright .NET — Installation
An MSTest class looks like this (the xUnit ones differ in the attributes shown in the table):
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace Bookshelf.Tests;
[TestClass]
public class BookshelfTests : PageTest
{
public override BrowserNewContextOptions ContextOptions() => new()
{
BaseURL = Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost:3000",
Locale = "en-GB",
};
[TestMethod]
public Task SearchFindsABook() => SearchFindsABookBody();
[TestMethod]
public Task ReaderAddsAReview() => ReaderAddsAReviewBody();
// SearchFindsABookBody() and ReaderAddsAReviewBody() hold the steps shown for NUnit.
}
Source: Playwright .NET — Test Runners
dotnet test flags, environment variables and .runsettings
Playwright’s .NET integration reads its configuration from environment variables, from Playwright.* parameters passed
after --, and from a .runsettings file (used by the dotnet test --settings option and by Visual Studio).
| Command or variable | Effect |
|---|---|
|
Run all tests on Chromium, headless. |
|
Choose |
|
Run headed (PowerShell: |
|
Open the Playwright Inspector. |
|
Same as |
|
Launch options from the command line. |
|
Take every setting from a file; the documented way to run several browser configurations is one |
|
Run tests by name; |
|
Number of parallel workers (NUnit). Only |
|
Workers for MSTest; parallelism is at class level ( |
|
Threads for xUnit and xUnit v3; the conservative algorithm is the default in xUnit 2.8 and later. |
|
Verbose API log on standard error, shown in the test log of each test. |
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<NUnit>
<NumberOfTestWorkers>2</NumberOfTestWorkers>
</NUnit>
<RunConfiguration>
<EnvironmentVariables>
<DEBUG>pw:api</DEBUG>
</EnvironmentVariables>
</RunConfiguration>
<Playwright>
<BrowserName>webkit</BrowserName>
<ExpectTimeout>5000</ExpectTimeout>
<LaunchOptions>
<Headless>true</Headless>
</LaunchOptions>
</Playwright>
</RunSettings>
$ BROWSER=webkit dotnet test --no-build
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, Duration: 1 s - NUnitTests.dll (net10.0)
$ dotnet test --no-build -- Playwright.BrowserName=webkit Playwright.LaunchOptions.Headless=true NUnit.NumberOfTestWorkers=2
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, Duration: 1 s - NUnitTests.dll (net10.0)
$ dotnet test --no-build --settings bookshelf.runsettings --logger "console;verbosity=detailed"
2026-10-25T19:58:18.017Z pw:api → POST http://localhost:3920/api/test/reset
...
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, Duration: 2 s - NUnitTests.dll (net10.0)
Source: Playwright .NET — Test Runners
The .runsettings element names depend on the runner: NUnit with NumberOfTestWorkers, MSTest with
Parallelize (Workers and Scope), xUnit with MaxParallelThreads. Browser selection, ExpectTimeout (the default
timeout of Expect assertions) and LaunchOptions are shared by all of them. Per-test context options, in contrast, are
set in code by overriding ContextOptions(), as above. HEADED=1, PWDEBUG=1 and a headed .runsettings were not
run, because they open windows.
Tracing in .NET
The .NET base classes do not have a tracing switch (the Python plugin has --tracing, and Java’s @UsePlaywright
has Options.setTrace). Start and stop it in [SetUp] and [TearDown] and keep the file only for failing tests. This
class ran; the intentionally failing test is marked [Explicit] so a normal dotnet test skips it:
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
namespace Bookshelf.Tests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class TracedTests : PageTest
{
public override BrowserNewContextOptions ContextOptions() => new()
{
BaseURL = Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost:3000",
};
[SetUp]
public async Task StartTracing()
{
// The NUnit base classes have no tracing switch: start it per test.
await Context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true });
}
[TearDown]
public async Task StopTracing()
{
var failed = TestContext.CurrentContext.Result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed;
await Context.Tracing.StopAsync(new()
{
// Keep the trace only for failing tests.
Path = failed ? Path.Combine(TestContext.CurrentContext.WorkDirectory, "traces",
$"{TestContext.CurrentContext.Test.Name}.zip") : null,
});
}
[Test]
public async Task CatalogueShowsSixBooksPerPage()
{
await Page.GotoAsync("/books");
await Expect(Page.GetByTestId("book-card")).ToHaveCountAsync(6);
}
[Test, Explicit("Fails on purpose to show the kept trace")]
public async Task FailingTestKeepsItsTrace()
{
await Page.GotoAsync("/books");
await Expect(Page.GetByTestId("book-card")).ToHaveCountAsync(7, new() { Timeout = 1000 });
}
}
Source: Playwright .NET — Trace viewer
$ dotnet test
Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3, Duration: 957 ms - NUnitTests.dll (net10.0)
$ dotnet test --filter "Name=FailingTestKeepsItsTrace"
Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1
Microsoft.Playwright.PlaywrightException : Locator expected to have count '7'
Call log:
- waiting for GetByTestId("book-card")
11 × locator resolved to 6 elements
- unexpected value "6"
$ ls bin/Debug/net10.0/traces
FailingTestKeepsItsTrace.zip
Source: Playwright .NET — Trace viewer
A failing assertion raises PlaywrightException (not the framework’s assertion exception), with the same call log as
the other languages. Open the trace with pwsh bin/Debug/net10.0/playwright.ps1 show-trace traces/FailingTestKeepsItsTrace.zip
or the cli.js form above; that was not run because it opens a window.
What differs between the languages
The browser engine, auto-waiting, actionability checks, locators and retrying assertions are shared. What differs is the tooling around them:
| Capability | Node.js (Playwright Test) | Python (pytest-playwright) | Java | .NET |
|---|---|---|---|---|
Configuration file |
|
|
Code: |
|
Projects (browser or device matrix) |
|
Repeat |
Your own parameterization; no built-in matrix |
One |
Parallelism |
|
|
JUnit parallel properties, one |
NUnit, MSTest and xUnit worker settings |
Sharding across machines |
|
None built in; split the test list with pytest selection or a CI matrix |
None built in; split with Surefire filters or a CI matrix |
None built in; split with |
Retries |
|
Not part of the plugin; use a pytest plugin for reruns |
Not part of Playwright; use the runner’s rerun support |
Not part of Playwright; use the runner’s retry feature |
Reporters |
HTML, JSON, JUnit, blob, list, custom |
pytest’s own reporting and plugins |
Surefire and JUnit XML reports |
|
Tracing |
|
|
|
|
Video and screenshots |
|
|
|
|
Assertions |
|
|
|
|
Visual comparison ( |
Yes |
No equivalent in |
No equivalent in |
No equivalent in |
Aria snapshot assertion |
|
|
|
|
UI Mode, component testing |
Yes |
Documented only for Node.js |
Documented only for Node.js |
Documented only for Node.js |
Codegen, trace viewer, Inspector |
Yes |
Yes |
Yes |
Yes |
Source: Playwright — Supported languages
Cells that say "none built in" or "no equivalent" mean that the language’s Playwright documentation and API reference
list no such feature at the check date (for example, the Python, Java and .NET PageAssertions and LocatorAssertions
classes have no screenshot assertion). They do not claim that it is impossible: screenshots can be taken in every
language and compared with an image-diff library or a hosted service, as explained in
Visual Regression Testing. The rows about sharding and retries describe
what Playwright itself provides; each runner has its own rerun and splitting ecosystem.
Other differences that matter every day:
-
Naming. The same API is
get_by_role,getByRoleandGetByRole; Python and C# addasyncflavors or theAsyncsuffix, and Python options are keyword arguments where Java uses builders (new Page.GetByRoleOptions().setName(...)) and C# usesnew() { Name = ... }. -
Enums. Roles are strings in Python (
"button"),AriaRole.BUTTONin Java andAriaRole.Buttonin .NET. -
Errors. A failed assertion is a Python
AssertionError, a JUnitAssertionFailedErroror a .NETPlaywrightException; the call log is identical. -
Default timeouts. Actions wait up to 30 s everywhere, retrying assertions wait 5 s, and each runner has its own test timeout (pytest has none by default; Playwright Test has 30 s; NUnit, MSTest and xUnit have none unless configured).
-
Thread safety. A Java
Playwrightinstance and everything created from it must stay on one thread. Python’s sync API runs a greenlet-based event loop of its own, so it cannot be used inside a runningasyncioloop; use the async API there. -
Version lag. The Node.js package is the first to receive a release; the Python, Java and .NET release-notes pages at the check date list 1.63 as their newest entry, while the registries already carried 1.64.x.
Choosing between them
| Situation | Suggested choice |
|---|---|
A team that already writes Node.js or TypeScript, or wants the most complete tooling |
Playwright Test in Node.js: config, projects, sharding, UI Mode, reporters and visual comparisons come built in. |
A Python codebase (Django, FastAPI, data science) |
|
A Java or Spring Boot team |
The plain library with JUnit 5 lifecycle methods, or |
A .NET team |
The NUnit, MSTest or xUnit base class matching your existing runner. |
Mixed teams |
Test the UI in one language and put API checks in each service’s own runner. Playwright’s |
Source: Playwright — Supported languages
Running in CI
The official Docker images exist per language and release: mcr.microsoft.com/playwright/python:v1.63.0-noble,
mcr.microsoft.com/playwright/java:v1.63.0-noble and mcr.microsoft.com/playwright/dotnet:v1.63.0-noble, next to the
Node.js image mcr.microsoft.com/playwright:v1.64.1-noble used elsewhere in this section. The image tag must match the
Playwright version of your project, because each version expects the browser builds it was tested with. They bundle the
browsers and operating-system dependencies, so no install step is needed in the job. The images were not pulled or run
for this page; the pipeline patterns, the trace artifact upload and flakiness handling are in
Running in CI/CD.
Source: Playwright Python — Docker
Common pitfalls
-
Forgetting the browser install. The library does not download browsers at
pip installtime. In Python runplaywright install; in .NET run the installation script after the first build; Java downloads on firstPlaywright.create(), which makes a first CI run slow and network-dependent. -
Mismatched runner and package majors in .NET. See the MSTest finding above.
-
exec:javawith a test-scoped dependency. Add-D exec.classpathScope=testor move the dependency to compile scope. -
Baking the base URL into tests. Use
--base-url,Options.setBaseUrlorContextOptions()so the same tests run against local, staging and CI hosts. -
Sharing state across tests. Contexts are cheap and isolated; browsers are shared. Do not reuse a page between tests, and do not run a global reset from every parallel worker.
-
Mixing the sync and async Python APIs. The sync API fails inside a running event loop. Keep sync and async suites in separate directories, as in the example above where the async tests had their own environment.
-
Using one
Playwrightobject from several Java threads. Create one per thread. -
Expecting runner flags to affect objects you create. The
pytest-playwrightflags apply to the built-in fixtures only; contexts created withbrowser.new_context()need their own arguments.
What changed recently
| Version | Change |
|---|---|
1.64 (Python 1.64.1, Java 1.64.0, .NET 1.64.1 published) |
Bindings published after the 1.63 documentation. Not exercised on this page. |
1.63 |
|
1.62 |
WebP screenshots ( |
1.55 |
|
1.42 |
Experimental JUnit integration ( |
Node-only releases |
Features such as test locks (1.63), the new component-testing model (1.62), WebMCP (1.64) and |