Start Small, Then Build Like a Professional
Your first Playwright test does not need to be complicated. Start by launching a browser, opening a page, performing an action and validating the result. As you become comfortable, introduce fixtures, reusable page objects, test data, API calls, authentication and CI/CD execution.
Browser
Launch Chromium, Firefox or WebKit.
Page
Navigate to a web application and interact with it.
Locator
Find buttons, fields, links and other elements.
Assertion
Verify that the application behaves as expected.
The simplest Playwright program launches a browser, opens a website and prints its title.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()
Chromium opens, navigates to example.com, prints the page title and then closes.
In a real automation project, tests are commonly organized using pytest.
from playwright.sync_api import sync_playwright
def test_homepage():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
assert "Example" in page.title()
browser.close()
Real applications contain text boxes, buttons, links, checkboxes and dropdowns. Playwright uses locators to interact with these elements.
# Fill text field
page.get_by_label("Username").fill("john")
# Fill password
page.get_by_label("Password").fill("secret123")
# Click button
page.get_by_role("button", name="Login").click()
# Click link
page.get_by_role("link", name="Products").click()
# Check checkbox
page.get_by_label("Remember me").check()
# Select dropdown option
page.get_by_label("Country").select_option("India")
Prefer meaningful locators such as get_by_role() and get_by_label() instead of relying heavily on fragile CSS or XPath selectors.
A test becomes useful when it verifies expected behavior.
from playwright.sync_api import expect
expect(page).to_have_title("My Application")
expect(
page.get_by_role("heading", name="Dashboard")
).to_be_visible()
expect(
page.get_by_text("Login successful")
).to_be_visible()
Do not only perform an action. Ask: "What should happen after this action?" That expected behavior becomes your assertion.
from playwright.sync_api import sync_playwright, expect
def test_login():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://your-app.example/login")
page.get_by_label("Username").fill("testuser")
page.get_by_label("Password").fill("Password123")
page.get_by_role("button", name="Login").click()
expect(
page.get_by_role("heading", name="Dashboard")
).to_be_visible()
browser.close()
Use test accounts and environment variables for credentials. Avoid committing real passwords into source control.
Opening a browser manually inside every test creates duplicated code. Playwright's pytest integration provides fixtures such as page.
from playwright.sync_api import Page, expect
def test_homepage(page: Page):
page.goto("https://example.com")
expect(page).to_have_title("Example Domain")
$ playwright install
$ pytest -v
from playwright.sync_api import Page, expect
def test_registration_validation(page: Page):
page.goto("https://your-app.example/register")
page.get_by_label("Email").fill("invalid-email")
page.get_by_role(
"button",
name="Create Account"
).click()
expect(
page.get_by_text("Enter a valid email")
).to_be_visible()
Real business flows often span multiple pages.
def test_purchase_flow(page):
page.goto("https://shop.example")
page.get_by_role(
"link",
name="Products"
).click()
page.get_by_text("Laptop").click()
page.get_by_role(
"button",
name="Add to Cart"
).click()
page.get_by_role(
"link",
name="Cart"
).click()
expect(
page.get_by_text("Laptop")
).to_be_visible()
When a test suite grows, page objects help keep locators and application interactions reusable.
from playwright.sync_api import Page
class LoginPage:
def __init__(self, page: Page):
self.page = page
self.username = page.get_by_label("Username")
self.password = page.get_by_label("Password")
self.login_button = page.get_by_role(
"button",
name="Login"
)
def open(self):
self.page.goto(
"https://your-app.example/login"
)
def login(self, username, password):
self.username.fill(username)
self.password.fill(password)
self.login_button.click()
from playwright.sync_api import expect
from pages.login_page import LoginPage
def test_login(page):
login_page = LoginPage(page)
login_page.open()
login_page.login(
"testuser",
"Password123"
)
expect(
page.get_by_role(
"heading",
name="Dashboard"
)
).to_be_visible()
If the login button changes later, you can update the locator in the page object instead of modifying every test that uses login.
Professional test suites frequently execute the same scenario against multiple data sets.
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.parametrize(
"username,password",
[
("user1", "Password1"),
("user2", "Password2"),
("user3", "Password3")
]
)
def test_login_users(
page: Page,
username,
password
):
page.goto(
"https://your-app.example/login"
)
page.get_by_label(
"Username"
).fill(username)
page.get_by_label(
"Password"
).fill(password)
page.get_by_role(
"button",
name="Login"
).click()
expect(
page.get_by_role(
"heading",
name="Dashboard"
)
).to_be_visible()
Logging in before every test can make a large suite repetitive. Playwright supports saving authentication state so tests can start from an already authenticated session.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
page.goto(
"https://your-app.example/login"
)
page.get_by_label("Username").fill("testuser")
page.get_by_label("Password").fill("Password123")
page.get_by_role(
"button",
name="Login"
).click()
page.wait_for_url("**/dashboard")
context.storage_state(
path="auth.json"
)
browser.close()
Later tests can create a browser context using the saved authentication state instead of repeating the complete login flow.
A real application may require testing interactions between two different users, such as an administrator and a customer.
def test_admin_customer_workflow(browser):
admin_context = browser.new_context()
customer_context = browser.new_context()
admin_page = admin_context.new_page()
customer_page = customer_context.new_page()
admin_page.goto(
"https://your-app.example/admin"
)
customer_page.goto(
"https://your-app.example"
)
# Admin performs an operation
# Customer verifies the result
admin_context.close()
customer_context.close()
Professional automation can combine API setup with UI validation. For example, create test data through an API and then verify that the same data appears in the UI.
def test_api_data_visible_in_ui(
page,
request
):
response = request.post(
"https://api.example.test/users",
data={
"name": "John Doe",
"email": "john@example.test"
}
)
assert response.ok
page.goto(
"https://your-app.example/users"
)
expect(
page.get_by_text("John Doe")
).to_be_visible()
API calls can often prepare test data faster than navigating through many UI screens, while the UI test verifies the user-facing behavior.
Playwright can intercept requests and provide controlled responses. This is useful when a test needs predictable backend data.
def test_mock_products(page):
page.route(
"**/api/products",
lambda route: route.fulfill(
status=200,
content_type="application/json",
body='''{
"products": [
{
"id": 1,
"name": "Test Laptop",
"price": 999
}
]
}'''
)
)
page.goto(
"https://your-app.example/products"
)
expect(
page.get_by_text("Test Laptop")
).to_be_visible()
page.screenshot(
path="screenshots/homepage.png",
full_page=True
)
html = page.content()
with open(
"page-source.html",
"w",
encoding="utf-8"
) as file:
file.write(html)
When a test fails, capture useful evidence such as screenshots, traces, logs and relevant application state rather than simply rerunning the test.
Large test suites can take considerable time when every test runs sequentially. pytest-based Playwright projects can use parallel execution tools such as pytest-xdist.
pip install pytest-xdist
pytest -n 4
Parallel tests should be designed so that they do not unintentionally depend on shared mutable data or the state created by another test.
Python Playwright provides both synchronous and asynchronous APIs. The async API can be useful when your application or test architecture already uses asyncio.
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()
await page.goto(
"https://example.com"
)
print(await page.title())
await browser.close()
asyncio.run(main())
The following example combines fixtures, page objects, locators and assertions into a maintainable test flow.
from playwright.sync_api import Page
class LoginPage:
def __init__(self, page: Page):
self.page = page
self.username = page.get_by_label(
"Username"
)
self.password = page.get_by_label(
"Password"
)
self.login_button = page.get_by_role(
"button",
name="Login"
)
self.dashboard = page.get_by_role(
"heading",
name="Dashboard"
)
def open(self):
self.page.goto(
"https://your-app.example/login"
)
def login(self, username, password):
self.username.fill(username)
self.password.fill(password)
self.login_button.click()
from playwright.sync_api import Page, expect
from pages.login_page import LoginPage
def test_valid_login(page: Page):
login = LoginPage(page)
login.open()
login.login(
"testuser",
"Password123"
)
expect(
login.dashboard
).to_be_visible()
The test focuses on business behavior while the page object owns the UI locators and interactions. This separation makes the suite easier to maintain as the application grows.
| Level | Focus | Typical Skills | Example |
|---|---|---|---|
| Beginner | Browser Basics | Browser, page, navigation | Open website |
| Beginner+ | Locators | Role, label, text | Login form |
| Intermediate | Assertions | Visibility, text, URL | Form validation |
| Intermediate+ | Fixtures | pytest fixtures | Reusable page |
| Advanced | Architecture | Page Object Model | Enterprise framework |
| Advanced+ | Integration | API + UI, auth, network | Business workflows |
| Professional | Scale | Parallelism, CI/CD, reporting | Production automation |
playwright-project/
│
├── .venv/
│
├── tests/
│ ├── test_login.py
│ ├── test_registration.py
│ ├── test_search.py
│ └── test_checkout.py
│
├── pages/
│ ├── login_page.py
│ ├── home_page.py
│ ├── product_page.py
│ └── checkout_page.py
│
├── fixtures/
│ └── test_fixtures.py
│
├── test-data/
│ ├── users.json
│ └── products.json
│
├── utils/
│ ├── api_helper.py
│ └── data_helper.py
│
├── screenshots/
│
├── reports/
│
├── requirements.txt
│
└── README.md
Your First Test Is the Beginning
A professional Playwright engineer does not simply know how to click a button with automation. The goal is to build reliable, readable, maintainable and scalable tests that validate real business behavior.
Playwright Locators & Selectors — learn get_by_role(), get_by_label(), get_by_text(), get_by_placeholder(), CSS selectors, XPath, locator chaining and strategies for choosing stable locators.