Here's a scene every team knows. A change ships. The tests are green. The server returns 200. And then a customer emails a screenshot: the "Submit" button does nothing on their phone, and it's been broken for three days.
Nothing failed. The unit tests checked the functions. The API returned the right JSON. But no machine ever did the one thing that matters — open the page in a real browser and try to use it. That gap is exactly what Playwright fills, and it's the reason it's become the most useful tool in my Django kit that isn't Django itself.
This is the first of a four-part series. Today is the on-ramp: what Playwright is, why it matters, and the surprising range of things it can do — written so a developer gets the depth and the people who fund and lead the work get the point. (The next posts get hands-on: a full Django setup tutorial, then two real stories from building VanillaPM — automating a couple hundred product screenshots, and hunting a CSS bug that had no visible element.)
What Playwright actually is
Playwright is a tool that drives a real web browser the way a person would — it opens a page, clicks buttons, fills forms, waits for things to appear, and can then check what happened, take a screenshot, or measure the page. It's open source, made by Microsoft, and it speaks Python (and JavaScript, Java, and .NET).
The whole idea fits in one snippet:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
print(page.title()) # "Example Domain"
page.screenshot(path="it-works.png")
browser.close()
That's it. You just ran a real Chromium browser, loaded a page, read its title, and saved a picture of it — from Python, in ten lines. Everything else is variations on that theme.
Why it's necessary: the gap manual testing leaves
Most test suites are made of unit tests — they check a function returns the right value — and maybe API tests — they check an endpoint returns the right JSON. Those are essential and fast, and you should have hundreds of them. (VanillaPM has over two thousand.)
But none of them answer the question your users actually care about: can a human open this page and get their job done? Modern Django apps make that gap wider, not smaller — the moment you sprinkle in Alpine, HTMX, or any JavaScript, the server can return a perfect 200 while the rendered page is broken. Django's own test client doesn't run JavaScript; it never sees the bug.
The traditional patch is manual QA — a person clicks through the critical screens before each release. That works right up until it doesn't:
- It's slow and repetitive — the same clicks, every release, forever.
- It's inconsistent — humans get bored, skip steps, and test different things each time.
- It doesn't scale — ten features become a hundred, and no one can re-click everything.
Playwright turns that click-through into code that runs in seconds, the same way, every single time — in your CI pipeline, before anything reaches a user.
💼 For the people who sign off on the work
(A lane for business and policy leaders — skip ahead if you're here for the code.)
Strip away the tooling and the pitch is simple: software that isn't tested by machines is tested by your customers. Automated browser testing moves the moment you discover a broken screen from "a user reported it" to "the pipeline caught it before it shipped."
Why that matters where it counts:
- The cost of a bug grows the later you find it. Caught in CI, a broken checkout flow costs a developer a few minutes. Caught in production, it costs support tickets, an emergency release, and — the expensive one — trust. Automated checks shift that cost left, to its cheapest point.
- It lets a small team move fast without breaking what already works. This is the real unlock. One developer built VanillaPM — a fifty-module platform — and ships changes confidently because 2,225 automated checks run on every change and scream if anything regresses. That's not heroics; it's leverage. Automation is how few people safely maintain a lot of software.
- It turns "done" into something you can verify. For anyone commissioning software — including public-sector and regulated buyers — "the critical user journeys are covered by automated browser tests" is a concrete, auditable acceptance criterion. It's a line you can put in a contract and a vendor can prove, instead of taking "it works" on faith.
The one-line version for a board slide: automated testing is insurance against your own releases — a one-time build that pays out on every change, forever.
What it can do (the part that surprises people)
"Playwright is for testing" is true but undersells it badly. Because it can drive a real browser and read anything on the page, the same tool covers a remarkable spread of jobs:
End-to-end tests — the headline use. Prove a real journey works, start to finish:
def test_user_can_log_in(page):
page.goto("http://localhost:8000/login/")
page.fill("#email", "demo@example.com")
page.fill("#password", "secret")
page.click("button[type=submit]")
assert page.get_by_role("heading", name="Dashboard").is_visible()
Screenshots, on demand — point it at any screen and capture a pixel-perfect image. Great for docs, marketing, and visual-regression checks:
page.screenshot(path="dashboard.png", full_page=True)
PDF generation — render any page straight to a PDF (headless Chromium):
page.pdf(path="report.pdf", format="A4")
Scraping & data extraction — read content from pages that only exist after JavaScript runs, which plain requests + BeautifulSoup can't reach:
titles = page.locator("h2.product-title").all_text_contents()
Cross-browser, from one script — the same code runs on Chromium, Firefox, and WebKit (Safari's engine), so you catch the "works in Chrome, broken in Safari" class of bug:
for engine in [p.chromium, p.firefox, p.webkit]:
browser = engine.launch()
# ... run the same checks ...
Mobile emulation — load any page as a specific device, with the right viewport and touch behaviour:
iphone = p.devices["iPhone 13"]
context = browser.new_context(**iphone)
Debugging — and this is the sleeper. Because you can run arbitrary JavaScript in the page and read the result, Playwright becomes a measuring instrument:
overflow = page.evaluate(
"() => document.documentElement.scrollWidth - document.documentElement.clientWidth"
)
That one line is how I pinned down a mobile layout bug that no human eye could isolate — the page scrolled sideways but nothing on screen looked wrong. (That's the war story in Part 4 of this series.)
In VanillaPM, Playwright does three of these jobs in production-adjacent anger: it runs the end-to-end smoke tests, it captures a couple hundred product screenshots for the investor deck and docs on command, and it's my go-to debugging probe. One tool, three very different jobs.
Why Django especially
Two reasons Playwright and Django are a natural pair:
- The JavaScript gap is real in Django apps. Django's test client is excellent and fast — but it's HTML-only. The moment your templates lean on Alpine or HTMX for interactivity, you need something that executes that JavaScript to know the page actually works. Playwright is that something.
-
Django hands you a server to test against.
StaticLiveServerTestCasespins up a real, running instance of your app inside the test — the perfect target for Playwright to drive. The two slot together with almost no glue. (That's the whole of Part 2.)
🧭 For engineering leaders
(A lane for the people deciding whether to adopt this.)
A few framing calls worth making deliberately:
- It's the thin top of the testing pyramid, not a replacement for it. End-to-end tests are powerful but slow and comparatively expensive to maintain. Keep the base of the pyramid wide (fast unit tests), and use Playwright for a small, high-value set of critical journeys — login, checkout, the one flow that must never break. Teams that try to test everything through the browser end up with a slow, flaky suite they learn to ignore.
- Budget honestly for flakiness. Browser tests fail for boring reasons — a selector moves, a timing assumption breaks. (I recently spent an afternoon on a test that failed only on CI because of filesystem ordering — the flaky-test tax is real.) Playwright's auto-waiting removes most of it, but plan for some maintenance rather than pretending it's free.
-
The build-vs-buy math is friendly. It's open source with no per-seat licensing, and one tool covers three browser engines, mobile emulation, a recorder that writes tests for you (
playwright codegen), and a trace viewer that replays a failed run step by step. The adoption cost is genuinely low — which matters for a small team or a tight budget. - The ROI is bigger than "tests." The same capability de-risks refactors (change internals confidently behind a safety net), gates your CI, and doubles as automation infrastructure — screenshots, PDFs, uptime checks. You buy one skill and get several capabilities.
The honest caveats
This crowd rewards honesty, so: Playwright is not a silver bullet. End-to-end tests are slower than unit tests, they can be flaky if you lean on brittle selectors, and they need upkeep as the UI changes. The discipline is to use them surgically — for the handful of journeys that genuinely matter — and let fast unit tests do the heavy lifting underneath. Reach for the browser when, and only when, the thing you need to verify only exists in the browser.
What's next
That's the why. The rest of this series gets concrete:
-
Part 2 — the tutorial: setting up Playwright with Django properly, start to finish (the
StaticLiveServerTestCasepattern, logging in once, selectors that don't rot, CI). - Part 3 — automation: how I automated a couple hundred product screenshots with one script.
- Part 4 — debugging: finding a CSS bug that had no visible element.
If you've been meaning to close the gap between "the tests pass" and "the page actually works," this is the tool that does it.
I write this series while building VanillaPM — a free, full-lifecycle project-management platform — in the open. Follow along for more build-in-public engineering.
Top comments (0)