Getting Started with Playwright Testing in Python
Automated testing is an essential aspect of modern web application development. It helps ensure that your application works correctly across different browsers and platforms, and that new changes don't break existing functionality.
While there are several testing frameworks available, Playwright has emerged as a powerful option that offers robust capabilities for testing web applications.
In this guide, we'll explore how to use Playwright with Python to create effective, reliable end-to-end tests for web applications. We'll cover everything from basic setup to advanced features, providing practical examples along the way.
What is Playwright?
Playwright is a modern browser automation framework that allows you to control Chromium, Firefox, and WebKit browsers with a single API. It was developed by Microsoft and has gained popularity due to its cross-browser support, reliability, and powerful features.
When used with Python, Playwright enables developers and QA engineers to write concise, reliable tests that can detect issues across different browsers. Its architecture is designed to be fast and dependable, with built-in auto-waiting mechanisms that reduce the flakiness that plagues many end-to-end testing solutions.
Some key benefits of Playwright for Python developers include:
- Support for all major browser engines (Chromium, Firefox, WebKit)
- Powerful auto-waiting mechanisms that improve test reliability
- Headless and headed mode for debugging
- Ability to test responsive designs and mobile viewports
- Network interception capabilities
- Strong isolation between test cases
Let's dive into setting up Playwright and creating our first test.
Setting up your environment
To get started with Playwright for Python, you'll need to set up your environment. This involves installing Python (if you haven't already), Playwright, and the browser engines you want to test with.
First, ensure you have a recent version of Python installed, then create and activate a virtual environment for your testing project:
python -m venv venv
source venv/bin/activate
Now, install the Playwright package using pip
:
pip install playwright
After installing the package, you'll need to install the browser engines that Playwright will control:
playwright install
This command installs Chromium, Firefox, and WebKit browsers on your system. The output should look like this:
Downloading browsers...
Chromium downloaded to: /home/user/.cache/ms-playwright/chromium-1045
Firefox downloaded to: /home/user/.cache/ms-playwright/firefox-1365
WebKit downloaded to: /home/user/.cache/ms-playwright/webkit-1622
Browsers downloaded successfully.
Creating a project structure
Let's create a basic project structure for our tests:
mkdir playwright-demo && cd playwright-demo
mkdir tests
touch tests/__init__.py
touch tests/test_example.py
This creates a simple structure with a tests
directory where we'll put our
test files.
Writing your first Playwright test
Now that you have your environment set up, let's create a simple test. We'll test a basic scenario: navigating to a website and verifying its title.
Open the tests/test_example.py
file and add the following code:
import re
from playwright.sync_api import Page, expect
def test_has_title(page: Page):
# Navigate to the Playwright website
page.goto("https://playwright.dev/")
# Expect the page title to contain "Playwright"
expect(page).to_have_title(re.compile("Playwright"))
This simple test:
- Imports the necessary modules from Playwright.
- Defines a test function that takes a
page
parameter (which will be automatically provided by the Playwright test runner). - Navigates to the Playwright website.
- Verifies that the page title contains the word "Playwright".
Running your first test
To run this test, you'll need a test runner. Playwright works well with pytest, which is the recommended test runner for Python. Let's install it:
pip install pytest pytest-playwright
Now, run your test using pytest:
pytest tests/test_example.py -v
You should see output similar to:
============================== test session starts ==============================
platform linux -- Python 3.9.7, pytest-7.3.1, pluggy-1.0.0
rootdir: /home/user/playwright-demo
plugins: playwright-0.3.0
collected 1 item
tests/test_example.py::test_has_title PASSED [100%]
=============================== 1 passed in 5.78s ===============================
Congratulations! You've just run your first Playwright test with Python.
In the next section, we'll dive deeper into the core concepts of testing with Playwright.
Core testing concepts
Playwright uses a three-level architecture:
- Browser: The browser instance (Chromium, Firefox, WebKit).
- Context: An isolated browser session (similar to an incognito window).
- Page: A single tab within a browser context.
When you run tests with pytest-playwright
, the test runner automatically
handles the lifecycle of these objects. However, sometimes you might want more
control over them.
Here's an example of handling browser and context explicitly:
from playwright.sync_api import sync_playwright
def test_multiple_pages():
with sync_playwright() as p:
# Launch a browser
browser = p.chromium.launch(headless=False)
# Create a context
context = browser.new_context()
# Create multiple pages in the same context
page1 = context.new_page()
page1.goto("https://example.com")
page2 = context.new_page()
page2.goto("https://playwright.dev")
# Verify content on both pages
assert "Example Domain" in page1.title()
assert "Playwright" in page2.title()
# Clean up
context.close()
browser.close()
In this example, we explicitly create a browser, a context, and multiple pages. This approach gives you fine-grained control over the lifecycle of these objects.
Locating elements
Playwright provides several ways to locate elements on a page:
- CSS selectors
- Text content
- XPath
- Test IDs
- Accessibility selectors
Here's an example demonstrating different locator methods:
from playwright.sync_api import Page, expect
def test_different_locators(page: Page):
page.goto("https://demo.playwright.dev/todomvc")
# CSS selector
new_todo = page.locator(".new-todo")
# Locate by placeholder text
new_todo_alt = page.locator("input[placeholder='What needs to be done?']")
# Fill the input
new_todo.fill("Learn Playwright")
new_todo.press("Enter")
# Locate by text content
todo_item = page.locator("text=Learn Playwright")
expect(todo_item).to_be_visible()
# Locate by test ID (requires data-testid attribute in the HTML)
# todo_list = page.locator("data-testid=todo-list")
# Locate using XPath
completed_checkbox = page.locator("//li//input[@type='checkbox']")
completed_checkbox.click()
# Verify item is completed
completed_item = page.locator(".completed")
expect(completed_item).to_be_visible()
It's generally recommended to use locators that are resilient to UI changes. Test IDs are particularly good for this, as they don't change when the UI design is updated.
Interacting with elements
Playwright makes it easy to interact with page elements just like a real user would. Here are some common interactions:
from playwright.sync_api import Page, expect
def test_interactions(page: Page):
page.goto("https://demo.playwright.dev/todomvc")
# Type into an input field
new_todo = page.locator(".new-todo")
new_todo.fill("Learn Playwright")
# Press a key
new_todo.press("Enter")
# Click an element
page.locator(".toggle").click()
# Double-click
# page.locator(".some-element").dblclick()
# Hover over an element
page.locator(".clear-completed").hover()
# Drag and drop
# source = page.locator(".source-element")
# target = page.locator(".target-element")
# source.drag_to(target)
# Select option from dropdown
# dropdown = page.locator("select")
# dropdown.select_option(label="Option Text")
# Work with multiple elements
all_items = page.locator("li.todo")
assert all_items.count() == 1
# Get element text
item_text = page.locator("label").text_content()
assert item_text == "Learn Playwright"
Playwright's interaction methods are designed to wait for elements to be actionable before performing the action, which helps create more reliable tests.
Assertions and expectations
Playwright provides a rich set of assertions to verify conditions in your tests.
These are implemented through the expect
API:
from playwright.sync_api import Page, expect
def test_assertions(page: Page):
page.goto("https://demo.playwright.dev/todomvc")
# Add a todo item
new_todo = page.locator(".new-todo")
new_todo.fill("Learn Playwright assertions")
new_todo.press("Enter")
todo_item = page.locator(".todo-list li")
# Visibility assertions
expect(todo_item).to_be_visible()
# Text assertions
expect(todo_item).to_have_text("Learn Playwright assertions")
# Attribute assertions
expect(todo_item).not_to_have_class("completed")
# Mark item as completed
page.locator(".toggle").click()
# Now check it has the completed class
expect(todo_item).to_have_class("completed")
# Count assertions
page.locator(".new-todo").fill("Another item")
page.locator(".new-todo").press("Enter")
all_items = page.locator(".todo-list li")
expect(all_items).to_have_count(2)
# State assertions
completed_checkbox = page.locator(".toggle").first
expect(completed_checkbox).to_be_checked()
These assertions not only verify conditions but also automatically wait for those conditions to be met, which helps reduce test flakiness.
Testing real user scenarios
Now that you understand the basics, let's create a more realistic test that simulates a user interacting with a to-do application.
from playwright.sync_api import Page, expect
def test_todo_workflow(page: Page):
# Navigate to the TodoMVC application
page.goto("https://demo.playwright.dev/todomvc")
# Create several todo items
create_todo_items(page, ["Buy groceries", "Pay bills", "Call mom"])
# Verify all items are in the list
todo_items = page.locator(".todo-list li")
expect(todo_items).to_have_count(3)
expect(todo_items).to_have_text(["Buy groceries", "Pay bills", "Call mom"])
# Complete the second item
todo_items.nth(1).locator(".toggle").click()
# Verify the item is marked as completed
expect(todo_items.nth(1)).to_have_class("completed")
# Filter to show only active items
page.locator("text=Active").click()
# Verify only active items are shown
visible_items = page.locator(".todo-list li:visible")
expect(visible_items).to_have_count(2)
#
# # Filter to show only completed items
page.get_by_role("link", name="Completed").click()
# Verify only completed items
visible_items = page.locator(".todo-list li:visible")
expect(visible_items).to_have_count(1)
# Clear completed items
page.get_by_role("button", name="Clear completed").click()
# Verify the completed item was removed
page.get_by_role("link", name="All").click()
todo_items = page.locator(".todo-list li")
expect(todo_items).to_have_count(2)
def create_todo_items(page: Page, items):
"""Helper function to create multiple todo items."""
for item in items:
page.locator(".new-todo").fill(item)
page.locator(".new-todo").press("Enter")
This test simulates a complete user workflow:
- Creating multiple to-do items.
- Marking an item as completed.
- Filtering the list to show only active or completed items.
- Clearing completed items.
pytest tests/test_todo_app.py -v
Advanced testing features
Playwright provides several advanced features that make it a powerful tool for end-to-end testing. Let's explore some of these features.
Network interception and mocking
Playwright allows you to intercept and modify network requests, which is useful for testing how your application behaves with different API responses:
import json
from playwright.sync_api import Page, Route, Request, expect
def test_network_interception(page: Page):
# Mock API response for todos
def handle_route(route: Route, request: Request):
# Return a mock response
route.fulfill(
status=200,
content_type="application/json",
body=json.dumps([
{"id": "1", "title": "Mocked Todo 1", "completed": False},
{"id": "2", "title": "Mocked Todo 2", "completed": True}
])
)
# Route any request to the API endpoint
page.route("**/api/todos", handle_route)
# Navigate to the page that makes the API request
page.goto("https://some-todo-app.example/")
# Verify that the mocked data appears in the UI
todo_items = page.locator(".todo-list li")
expect(todo_items).to_have_count(2)
expect(todo_items.first).to_have_text("Mocked Todo 1")
expect(todo_items.nth(1)).to_have_class("completed")
This capability is particularly useful for testing edge cases and error conditions that might be difficult to reproduce with real API calls.
Authentication handling
Testing authenticated parts of your application can be challenging. Playwright makes this easier by allowing you to save and reuse authentication state:
from playwright.sync_api import Page, expect
def test_authenticated_workflow(page: Page):
# Navigate to login page
page.goto("https://demo.site/login")
# Log in
page.locator("input[name='username']").fill("test_user")
page.locator("input[name='password']").fill("password123")
page.locator("button[type='submit']").click()
# Wait for successful login (dashboard page loads)
expect(page.locator("h1")).to_have_text("Dashboard")
# Save authentication state for reuse
storage_state = page.context.storage_state()
with open("auth.json", "w") as f:
f.write(storage_state)
# In future tests, you can reuse this authentication state:
# browser.new_context(storage_state="auth.json")
By saving and reusing the authentication state, you can skip the login process in subsequent tests, making them faster and more focused.
Working with iframes and popups
Web applications often include iframes and popups, which can be tricky to test. Playwright provides mechanisms to interact with these elements:
from playwright.sync_api import Page, expect
def test_iframe_interaction(page: Page):
page.goto("https://page-with-iframe.example/")
# Get the iframe
frame = page.frame_locator(".iframe-class").first
# Interact with elements inside the iframe
frame.locator("button").click()
expect(frame.locator(".result")).to_have_text("Success")
def test_popup_handling(page: Page):
page.goto("https://page-with-popup.example/")
# Set up a listener for the popup
with page.expect_popup() as popup_info:
# Trigger the popup
page.locator("button#open-popup").click()
# Get the popup page
popup = popup_info.value
# Interact with the popup
popup.locator("button#confirm").click()
# Verify the result on the main page
expect(page.locator(".status")).to_have_text("Popup confirmed")
These examples demonstrate how Playwright allows you to interact with complex page structures, including nested frames and popup windows.
Debugging Playwright tests
When tests fail, debugging them can be challenging. Playwright provides several tools to help with this process.
The Playwright Inspector is a graphical interface that helps you debug your
tests. You can activate it by running your tests with the --debug
flag:
PWDEBUG=1 pytest tests/test_example.py
This opens the Inspector, which allows you to:
- Step through your test one action at a time.
- Inspect the page state at each step.
- Pick selectors visually.
- Adjust timeouts and other settings.
For more complex debugging scenarios, Playwright can record traces of your tests that can be viewed later:
import pytest
from playwright.sync_api import Page
@pytest.fixture(scope="function", autouse=True)
def setup_teardown(page: Page):
# Start tracing before each test
context = page.context
context.tracing.start(screenshots=True, snapshots=True)
yield
# Stop tracing and save the results
context.tracing.stop(path="trace.zip")
With this fixture, Playwright will record detailed traces of all your tests. You can view these traces using the Playwright Trace Viewer:
playwright show-trace trace.zip
The Trace Viewer shows:
- Screenshots at each step
- Network requests
- Console logs
- Source code
- And more
This tool is invaluable when debugging test failures, especially in CI environments where you can't directly observe the test execution.
Final thoughts
In this guide, we've explored how to set up Playwright, write basic and advanced tests, debug issues, and follow best practices for maintainable test code. These skills will help you build a reliable test suite that catches issues before they reach your users.
Thanks for reading, and happy testing!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github