# 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](https://betterstack.com/community/guides/testing/playwright-intro/) 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.

[ad-logs]

## What is Playwright?

![Playwright](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/6116942f-8e6e-44e6-b6e8-ec0281bd3800/md2x =1280x640)

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:

```command
python -m venv venv
```

```command
source venv/bin/activate
```

Now, install the Playwright package using `pip`:

```command
pip install playwright
```

After installing the package, you'll need to install the browser engines that
Playwright will control:

```command
playwright install
```

This command installs Chromium, Firefox, and WebKit browsers on your system. The
output should look like this:

```text
[output]
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.
```

![Playwright downloading browser](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c163d41e-bbb8-44e6-ef81-4fb565256e00/lg2x =1442x742)

### Creating a project structure

Let's create a basic project structure for our tests:

```command
mkdir playwright-demo && cd playwright-demo
```

```command
mkdir tests
```

```command
touch tests/__init__.py
```

```command
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:

```python
[label tests/test_example.py]
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:

1. Imports the necessary modules from Playwright.
2. Defines a test function that takes a `page` parameter (which will be
   automatically provided by the Playwright test runner).
3. Navigates to the Playwright website.
4. Verifies that the page title contains the word "Playwright".

![Playwright homepage](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/36ed872c-4a37-4055-7362-e20ea41b7400/orig =1783x1087)

### 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:

```command
pip install pytest pytest-playwright
```

Now, run your test using pytest:

```command
pytest tests/test_example.py -v
```

You should see output similar to:

```text
[output]
============================== 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 ===============================
```

![Playright first test](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/79349f9e-6869-47cd-8901-07e0cb175200/orig =1442x449)

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:

1. **Browser**: The browser instance (Chromium, Firefox, WebKit).
2. **Context**: An isolated browser session (similar to an incognito window).
3. **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:

```python
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:

```python
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:

```python
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:

```python
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")

[highlight]
    # 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")
[/highlight]

    # Mark item as completed
    page.locator(".toggle").click()

[highlight]
    # Now check it has the completed class
    expect(todo_item).to_have_class("completed")
[/highlight]

    # Count assertions
    page.locator(".new-todo").fill("Another item")
    page.locator(".new-todo").press("Enter")

    all_items = page.locator(".todo-list li")
[highlight]
    expect(all_items).to_have_count(2)
[/highlight]

    # State assertions
    completed_checkbox = page.locator(".toggle").first
[highlight]
    expect(completed_checkbox).to_be_checked()
[/highlight]
```

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.

```python
[label tests/test_todo_app.py]
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:

1. Creating multiple to-do items.
2. Marking an item as completed.
3. Filtering the list to show only active or completed items.
4. Clearing completed items.

```command
pytest tests/test_todo_app.py -v
```

![Testing todo apps](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/565965e1-6bc0-4692-869d-c290060a6e00/public =1442x449)

## 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:

```python
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:

```python
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:

```python
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:

```command
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.

![Screenshot of Playwright Inspector](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/96e7ad75-2fc4-4ef2-9dd8-c2e7551ae300/md2x =734x743)

For more complex debugging scenarios, Playwright can record traces of your tests
that can be viewed later:

```python
[label tests/conftest.py]
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:

```command
playwright show-trace trace.zip
```

The Trace Viewer shows:

- Screenshots at each step
- Network requests
- Console logs
- Source code
- And more

![Screenshot of Trace Viewer](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/d8844c07-3c66-4d00-4e2b-245cbff39200/orig =1585x993)

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!
