Testing the Browser
Using Playwright to test the full application end to end
Concepts
- End-to-end tests drive a real browser and exercise the entire stack: HTML rendering, CSS, JavaScript, HTTP, server routing, and the database—all at once
page.goto(url)navigates;page.fill(selector, text)types into an input;page.click(selector)clicks an element;page.locator(selector)returns a reference to assert on- HTMX requests are asynchronous
- after clicking a button that triggers an HTMX request, the response has not arrived yet
- tests must wait using
expect(locator).to_be_visible()rather than asserting immediately
- A locator is a lazy reference to an element;
Playwright re-queries the DOM every time you use it
- stays valid even if the page updates between actions, unlike a snapshot taken at one moment
- End-to-end tests are slower and more likely to fail for non-code reasons
(network timing, browser quirks) than unit tests
- use them to verify complete user workflows before a release, not to check every individual function
What Playwright does
- A real browser (Chromium) controlled from Python
- The difference between unit tests and end-to-end tests
Installing and configuring Playwright
playwright installinside the uv environment- A taskipy task to run the suite
- Running headed vs. headless
A first browser test
- Navigating to the table page and asserting that rows appear
- Clicking a row to expand it
Testing form submission
- Filling inputs, clicking submit, asserting the new row appears
- Waiting for HTMX requests to complete
When end-to-end tests are worth the cost
- Slow, flaky, and hard to debug
- Run them before a release, not on every save
- Where they catch bugs that unit tests miss
Check for Understanding
What does end-to-end testing catch that unit tests of individual functions cannot?
End-to-end tests catch integration failures: the server returns the right data but the JavaScript does not insert it into the correct element; the form submits successfully but the HTMX swap target is wrong; the CSS hides an element that should be visible. These bugs live in the interactions between layers, not in any single function, so unit tests that mock each layer individually will not reveal them.
Why do Playwright tests need explicit waits after clicking HTMX-powered buttons?
When you click an element with an hx-get attribute, HTMX makes an asynchronous HTTP
request. The Python test code runs faster than the network round-trip; if you assert
immediately after clicking, the response has not yet arrived and the DOM has not been
updated. Playwright's expect(locator).to_be_visible() and similar methods wait for
the condition to become true (up to a timeout) rather than checking at a single instant.
When would you run end-to-end tests and when would you rely on unit tests instead?
Run unit tests on every save during development: they are fast (milliseconds each) and give immediate feedback. Run end-to-end tests before committing a significant feature, before a release, or after a deployment. End-to-end tests are worth the cost when verifying complete user workflows (submit a form, see the result in the table) or when a bug was caused by an integration failure that unit tests missed.
What is a "locator" in Playwright, and how is it different from finding an element with a CSS selector at a fixed point in time?
A locator is a description of how to find an element; Playwright re-evaluates it
against the current DOM every time you perform an action or assertion on it. This
means a locator does not go stale when the page updates. In contrast, methods like
querySelector return a specific DOM node at the moment they are called; if the
page re-renders (e.g., after an HTMX swap), the old reference may point to a node
that is no longer in the document.
Exercises
Test the search flow
Write a Playwright test that: (1) navigates to the table page, (2) types a known site name into the search input, (3) submits the search, (4) waits for the results to load, and (5) asserts that every visible row shows that site name and that at least one row is present.
Test navigation
Write a test that: (1) loads the first page of results, (2) clicks the "Next" link,
(3) waits for the second page to load, (4) asserts that the URL now contains
page=2, and (5) asserts that the rows shown are different from those on page 1.
Debug in headed mode
Take a test that is failing and run it in headed mode (--headed flag) so you can
watch the browser. Write a two-paragraph description of what you saw the browser do,
what you expected it to do, and what you learned from watching that you could not
have learned from the error message alone.
Compare test speeds
Run the full pytest unit-test suite and record the total time. Run the full Playwright suite and record its time. Compute the ratio. Given that ratio, write a brief recommendation for how often each suite should be run during a typical development session and before a release.