Playwright — UI Practical Playbook
Copy-paste recipes for common UI testing scenarios.
1) Auth with Storage State Reuse
Authenticate once, reuse across all tests — no repeated login:
# tests/conftest.py
import pytest
from playwright.sync_api import Playwright, Browser, BrowserContext
@pytest.fixture(scope="session")
def auth_state(playwright: Playwright):
"""Login once via browser, save cookies + localStorage."""
browser = playwright.chromium.launch()
context = browser.new_context()
page = context.new_page()
page.goto("https://example.com/login")
page.get_by_label("Email").fill("admin@example.com")
page.get_by_label("Password").fill("secret")
page.get_by_role("button", name="Sign in").click()
page.wait_for_url("**/dashboard")
state = context.storage_state() # captures cookies + localStorage
browser.close()
return state
@pytest.fixture
def auth_page(browser: Browser, auth_state: dict):
"""Every test gets a fresh page pre-loaded with auth cookies."""
context = browser.new_context(storage_state=auth_state)
page = context.new_page()
yield page
context.close()
2) Form Submission with Validation Errors
from playwright.sync_api import Page, expect
def test_form_shows_validation_errors(page: Page):
page.goto("/register")
# submit empty form
page.get_by_role("button", name="Register").click()
# verify inline validation messages appear
expect(page.get_by_text("Email is required")).to_be_visible()
expect(page.get_by_text("Password must be at least 8 characters")).to_be_visible()
# fix one field, verify its error disappears
page.get_by_label("Email").fill("alice@example.com")
page.get_by_role("button", name="Register").click()
expect(page.get_by_text("Email is required")).to_be_hidden()
3) File Download
from playwright.sync_api import Page
def test_download_report(page: Page, tmp_path):
page.goto("/reports")
# expect_download waits for the download event
with page.expect_download() as download_info:
page.get_by_role("button", name="Export CSV").click()
download = download_info.value
dest = tmp_path / download.suggested_filename
download.save_as(dest)
content = dest.read_text()
assert "user_id" in content # verify CSV header
assert len(content.splitlines()) > 1 # has data rows
4) Dialog Handling (Alert / Confirm)
from playwright.sync_api import Page, expect
def test_confirm_delete(page: Page):
page.goto("/users")
# register handler BEFORE the action that triggers the dialog
page.on("dialog", lambda dialog: dialog.accept())
page.get_by_role("row").filter(has_text="Alice").get_by_role("button", name="Delete").click()
# verify user removed after confirmation
expect(page.get_by_role("row").filter(has_text="Alice")).to_have_count(0)
def test_cancel_delete(page: Page):
page.goto("/users")
page.on("dialog", lambda dialog: dialog.dismiss())
page.get_by_role("row").filter(has_text="Bob").get_by_role("button", name="Delete").click()
# user still present after cancel
expect(page.get_by_role("row").filter(has_text="Bob")).to_be_visible()
5) Wait for API Response Before Assertion
from playwright.sync_api import Page, expect
def test_search_waits_for_api(page: Page):
page.goto("/search")
page.get_by_placeholder("Search...").fill("playwright")
# wait until the actual API response arrives
with page.expect_response("**/api/search*") as resp_info:
page.get_by_role("button", name="Search").click()
resp = resp_info.value
assert resp.status == 200
# now assert UI updated with results
expect(page.get_by_role("listitem")).to_have_count(10)
6) Multi-Tab / Popup
from playwright.sync_api import Page, expect
def test_external_link_opens_new_tab(page: Page):
page.goto("/help")
# expect_popup catches the new tab/window
with page.expect_popup() as popup_info:
page.get_by_role("link", name="Documentation").click()
new_tab = popup_info.value
new_tab.wait_for_load_state()
expect(new_tab).to_have_url("https://docs.example.com/")
7) Table Sorting and Filtering
from playwright.sync_api import Page, expect
def test_table_sort_by_name(page: Page):
page.goto("/users")
# click column header to sort
page.get_by_role("columnheader", name="Name").click()
# verify first row is alphabetically first
first_cell = page.get_by_role("row").nth(1).get_by_role("cell").first
expect(first_cell).to_contain_text("Alice")
def test_table_filter(page: Page):
page.goto("/users")
page.get_by_placeholder("Filter by role...").fill("admin")
# wait for filtered results to stabilize, then verify each data row
data_rows = page.get_by_role("row").filter(has_text="admin")
expect(data_rows).not_to_have_count(0)
for i in range(data_rows.count()):
expect(data_rows.nth(i)).to_contain_text("admin")
8) Screenshot Comparison (Visual Regression)
from playwright.sync_api import Page
def test_dashboard_visual(page: Page):
page.goto("/dashboard")
page.wait_for_load_state("networkidle")
# first run: use `pytest --update-snapshots` to generate baseline
# subsequent runs compare against the saved baseline
expect(page).to_have_screenshot("dashboard.png", max_diff_pixels=50)
Quick Checklist
- Use
storage_statefor auth — never repeat login flows in every test. - Register dialog handlers before the triggering action.
- Use
expect_download/expect_popup— not manual waits. tmp_pathfor downloaded files — auto-cleaned by pytest.expect_responseto sync UI with API — avoids flaky timing.