Using Playwright to test my static sites – alexwlchanSkip to main contentUsing Playwright to test my static sites<br>Posted 2 May 2026<br>Also filed in Static websites<br>I build a lot of static websites – including this site and all of my local media archives – and I want to test them. Most of my pages are static HTML and I can write automated tests that analyse the HTML, but for more complex sites I have JavaScript that runs in the browser and modifies the page. The only way to test that functionality is to open the page in a browser, click around, and see what happens. I could do that manually, but it quickly gets tedious.<br>To automate this process, I’ve been using a testing framework called Playwright, which is designed for this sort of end-to-end testing. It’s a tool that allows you to programatically control a web browser, look at the contents of a page, and make assertions about what’s there. Playwright can be used to test or script any kind of web app; I’m using it for static sites because those are the only web apps I have.<br>Playwright is available as a CLI, or there are libraries to use it with TypeScript, Python, .NET, and Java. All my other tests are written in Python, so that’s what I’m using.<br>Writing a basic test with Playwright<br>To set up Playwright with Python, you install the playwright library using pip or uv, then install a web browser for Playwright to control. (You can’t use Playwright with the browser you use day-to-day; you need special binaries with control hooks.)<br>I use Safari as my main browser, and Safari is based on WebKit, so let’s install that:<br>$ uv pip install playwright<br>$ python3 -m playwright install webkitThen we can start writing tests. Here’s a basic test in which Playwright launches WebKit, opens example.com, and checks the text Example domain is visible on the page:<br>from playwright.sync_api import expect, sync_playwright
def test_basic_playwright() -> None:<br>"""<br>Run a basic test with Playwright: load a web page and check it<br>contains the expected text.<br>"""<br>with sync_playwright() as p:<br>browser = p.webkit.launch()
page = browser.new_page()<br>page.goto("https://example.com/")<br>expect(page.get_by_text("Example domain")).to_be_visible()
browser.close()For a larger app, you might run your tests with multiple browsers to check compatibility – Playwright supports lots of other browsers, including Chromium, Firefox, and Mobile Safari in emulation. I’m just testing private sites where I’m the only user, so a single browser is fine.<br>This test passes in about half a second on my computer. That’s fine for a single test, but it would add up if I had lots of tests, each starting and stopping the browser every time. It would be nice to make that process faster, and to reduce some of the boilerplate as well.<br>A pair of Playwright fixtures<br>To reduce the repetition and reuse the browser instance, I have a couple of pytest fixtures to simplify things.<br>The first is a session-scoped fixture that starts the browser at the start of the test run, and closes it when I’m done:<br>from collections.abc import Iterator
from playwright.sync_api import Browser, sync_playwright<br>import pytest
@pytest.fixture(scope="session")<br>def browser() -> Iterator[Browser]:<br>"""<br>Launch an instance of WebKit to interact with in tests.<br>"""<br>with sync_playwright() as p:<br>webkit = p.webkit.launch()<br>yield webkit<br>webkit.close()Because this is a session-scoped fixture, it only runs once per test suite – that means the browser is only started once, then the same instance is reused for all the tests. This makes a large test suite significantly faster.<br>My other fixture is a bit more complicated – it gives you a page to interact with, and at the end of the test it checks the page didn’t have any warnings or errors. This is a strict approach, which helps me spot errors in areas I wasn’t explicitly testing. Here’s the fixture:<br>from collections.abc import Iterator
from playwright.sync_api import Browser, Page<br>import pytest
@pytest.fixture(scope="function")<br>def page(browser: Browser) -> Iterator[Page]:<br>"""<br>Open a new page in the browser.
If there are any errors or warnings when loading the page, the test<br>will fail when this fixture is cleaned up.<br>"""<br>p = browser.new_page()
# Capture anything that gets logged to the console.<br>console_messages = []<br>p.on("console", lambda msg: console_messages.append(msg))
# Capture any page errors<br>page_errors = []<br>p.on("pageerror", lambda err: page_errors.append(err))
yield p
# Check there weren't any console errors logged to the page.<br>console_errors = [<br>msg.text<br>for msg in console_messages<br>if msg.type == "error" or msg.type == "warning"<br>assert console_errors == []
# Check there weren't any page errors<br>assert page_errors == []These two fixtures allow for tighter, faster tests, focusing on what the test is actually checking. Here’s the example test, rewritten to use this fixture:<br>def test_playwright_with_fixture(page: Page) -> None:<br>"""<br>Run a test using my Playwright fixture: load a web page, check it<br>contains...