Testing in Practice · Testing APIs, Contracts and User Journeys
End-to-End Tests with Playwright
Drive a real browser through critical user journeys with Playwright, using role-based locators and auto-waiting assertions instead of sleeps.
What you'll build
An end-to-end test of a login page in a real Chromium browser: invalid credentials show an error, valid ones lead to the dashboard. The test uses role- and label-based locators (like a user, and like assistive technology) and web-first assertions that wait automatically.
The idea
- End-to-end tests are slow and costly; use them for a few critical journeys (sign-up, checkout, login), not every rule.
- Locate elements by role, label or text (
get_by_role,get_by_label), not CSS classes that change with styling. - Playwright auto-waits for elements to be ready, and
expect(...)assertions retry until they pass or time out: nosleep. - Record traces and screenshots on failure in CI to debug flaky runs.
The code
# file: site/index.html
<!doctype html>
<title>Shop login</title>
<main>
<h1>Sign in</h1>
<form id="login">
<label>Email <input name="email" type="email"></label>
<label>Password <input name="password" type="password"></label>
<button>Sign in</button>
<p role="alert" hidden></p>
</form>
</main>
<script>
document.getElementById("login").addEventListener("submit", (e) => {
e.preventDefault();
const ok = e.target.email.value === "[email protected]" && e.target.password.value === "correct-horse";
setTimeout(() => { // simulate a slow server
if (ok) { document.querySelector("main").innerHTML = "<h1>Dashboard</h1><p>Welcome back, Ana</p>"; }
else { const a = document.querySelector("[role=alert]"); a.textContent = "Wrong email or password"; a.hidden = false; }
}, 300);
});
</script>
# file: test_login.py
import functools
import http.server
import os
import threading
import pytest
from playwright.sync_api import expect, sync_playwright
@pytest.fixture(scope="session")
def base_url():
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory="site")
handler.log_message = lambda *args: None
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
yield f"http://127.0.0.1:{server.server_port}"
server.shutdown()
@pytest.fixture
def page():
with sync_playwright() as p:
browser = p.chromium.launch(executable_path=os.environ["PLAYWRIGHT_CHROMIUM"], args=["--no-sandbox"])
yield browser.new_page()
browser.close()
def login(page, email, password):
page.get_by_label("Email").fill(email)
page.get_by_label("Password").fill(password)
page.get_by_role("button", name="Sign in").click()
def test_wrong_password_shows_an_error(page, base_url):
page.goto(base_url)
login(page, "[email protected]", "nope")
expect(page.get_by_role("alert")).to_have_text("Wrong email or password") # waits for the slow response
def test_valid_login_reaches_the_dashboard(page, base_url):
page.goto(base_url)
login(page, "[email protected]", "correct-horse")
expect(page.get_by_role("heading")).to_have_text("Dashboard")
expect(page.get_by_text("Welcome back, Ana")).to_be_visible()
# file: main.py
import pytest
pytest.main(["-q", "--tb=short", "-p", "no:cacheprovider"])
Output
.. [100%] 2 passed in <time>
Run this on your computer — see “Get Started” for setup.
Common mistakes
time.sleep()waits, which are either too short (flaky) or too long (slow); use web-first assertions.- Locators tied to CSS classes or DOM structure, breaking on every redesign.
- Hundreds of end-to-end tests for logic that unit or API tests could cover.
- Sharing logged-in state between tests by accident; give each test a fresh context (or deliberately reuse saved storage state).
Key points
- Reserve end-to-end tests for critical journeys.
- Use role- and label-based locators and auto-waiting assertions.
- Capture traces on failure to debug CI runs.
Further reading: Playwright for Python · Locators · Best practices
Practice questions
Why prefer get_by_role and get_by_label over CSS selectors?
Answer
They target what users (and assistive technologies) see, so tests survive styling and markup refactors and encourage accessible pages.
How do Playwright assertions avoid sleeps?
Answer
Web-first assertions like expect(locator).to_have_text() retry until the condition holds or a timeout expires.
Which flows deserve end-to-end tests?
Answer
A small set of critical user journeys such as sign-up, login and checkout; other logic is cheaper to test at lower levels.
Open in the app to mark lessons complete, take notes and track your progress.