Skip to content

Playwright — UI Testing

Locators

Locators are live objects that auto-wait and re-evaluate when DOM changes.

Priority (most resilient first)

from playwright.sync_api import Page

# 1. Role — matches accessibility role + accessible name
page.get_by_role("button", name="Submit")
page.get_by_role("link", name="Sign in")
page.get_by_role("heading", name="Dashboard")

# 2. Label — form controls by associated <label>
page.get_by_label("Email address")

# 3. Placeholder — when no label exists
page.get_by_placeholder("Search...")

# 4. Text — static visible text content
page.get_by_text("Welcome back")
page.get_by_text("Order #", exact=False)  # partial match

# 5. Test ID — controlled escape hatch for dynamic/localized content
page.get_by_test_id("login-form")

# 6. CSS / XPath — last resort, brittle
page.locator(".btn-primary")
page.locator("//div[@data-id='123']")

Chaining and Filtering

# narrow scope: find button inside a specific card
card = page.locator(".product-card").filter(has_text="Pro Plan")
card.get_by_role("button", name="Buy").click()

# nth element from a list
page.get_by_role("listitem").nth(0).click()

# count elements
assert page.get_by_role("listitem").count() == 5

Actions

Playwright auto-waits for elements to be actionable (visible, enabled, stable) before each action.

# navigation
page.goto("https://example.com")
page.go_back()
page.reload()

# click
page.get_by_role("button", name="Submit").click()
page.get_by_role("button", name="Delete").dblclick()  # double click (also: click(click_count=2))

# fill form fields (clears existing value first)
page.get_by_label("Email").fill("alice@example.com")
page.get_by_label("Password").fill("secret123")

# type character by character (for autocomplete/search)
page.get_by_placeholder("Search").press_sequentially("playwright")

# select from dropdown
page.get_by_label("Country").select_option("Ukraine")

# checkbox / radio
page.get_by_role("checkbox", name="Accept terms").check()

# file upload
page.get_by_label("Upload").set_input_files("report.pdf")

# keyboard
page.get_by_label("Search").press("Enter")

# hover
page.get_by_text("Menu").hover()

Assertions

expect() assertions auto-retry until the condition is met or timeout expires.

import re
from playwright.sync_api import Page, expect


def test_login_flow(page: Page):
    page.goto("https://example.com/login")
    page.get_by_label("Email").fill("alice@example.com")
    page.get_by_label("Password").fill("secret")
    page.get_by_role("button", name="Sign in").click()

    # page-level assertions
    expect(page).to_have_url(re.compile(r"/dashboard"))
    expect(page).to_have_title(re.compile("Dashboard"))

    # element assertions
    expect(page.get_by_role("heading")).to_have_text("Welcome, Alice")
    expect(page.get_by_test_id("sidebar")).to_be_visible()

Common Assertions

Assertion Checks
to_be_visible() Element is visible (non-zero size, not display:none / visibility:hidden)
to_be_hidden() Element is not visible
to_be_enabled() / to_be_disabled() Interactive state
to_have_text("...") Full text match
to_contain_text("...") Partial text match
to_have_value("...") Input current value
to_have_attribute(name, value) HTML attribute
to_have_class("...") Exact class attribute value
to_have_count(n) Number of matching elements
to_have_url(pattern) Current page URL
to_have_title(pattern) Page title
to_be_checked() Checkbox / radio state

Auto-Waiting

Playwright waits automatically before actions:

Check When
Attached Element is in DOM
Visible Element has non-zero size and is not hidden
Stable Element is not animating
Enabled Element is not disabled
Receives Events Element is not obscured by another element

No need for time.sleep(), WebDriverWait, or explicit waits.


Test Isolation

Each test gets a fresh BrowserContext — isolated cookies, storage, and cache.

from playwright.sync_api import Page


def test_first(page: Page):
    page.goto("https://example.com")
    # this page has its own cookies and storage

def test_second(page: Page):
    page.goto("https://example.com")
    # completely isolated from test_first

Fixtures (pytest-playwright)

import pytest
from playwright.sync_api import Page, expect


@pytest.fixture(scope="function", autouse=True)
def navigate_to_app(page: Page):
    """Navigate to app before each test, cleanup after."""
    page.goto("https://example.com")
    yield
    # teardown runs after test


def test_homepage(page: Page):
    expect(page.get_by_role("heading")).to_have_text("Home")

Override context options per test with marker:

@pytest.mark.browser_context_args(locale="uk-UA", timezone_id="Europe/Kyiv")
def test_localized_ui(page: Page):
    page.goto("https://example.com")
    expect(page.get_by_text("Вітаємо")).to_be_visible()

Extra Python Methods (Cheat-Sheet Gaps)

from playwright.sync_api import Page, expect


def test_navigation_and_page_info(page: Page):
    page.goto("/home")
    page.get_by_role("link", name="Docs").click()
    page.go_back()
    page.go_forward()
    page.wait_for_load_state("domcontentloaded")

    # current URL is a property in Python sync API
    assert "/home" in page.url
    assert page.title() != ""


def test_additional_locators_and_actions(page: Page):
    page.goto("/board")

    # image by alt text
    page.get_by_alt_text("Company logo").click()

    # first matching element from a locator list
    first_card = page.locator(".task-card").first
    first_card.focus()
    first_card.scroll_into_view_if_needed()

    # drag-and-drop in Python API
    source = page.get_by_test_id("drag-source")
    target = page.get_by_test_id("drop-target")
    source.drag_to(target)

    # explicit checkbox clear state
    checkbox = page.get_by_role("checkbox", name="Receive updates")
    checkbox.uncheck()
    expect(checkbox).not_to_be_checked()

Use these only when needed; default to role/label locators and standard auto-wait behavior.