Playwright Intro - discussion (2023-01-04)
Playwright's intro docs are straightforward, but they highlight what I care about most: reliable flow tests, traceability, and real browser behavior.
If you're writing end-to-end tests, what do you treat as your stable contract (URLs, route titles, rendered markers)? Do you prefer screenshot assertions as evidence, or do they become brittle over time? How do you keep e2e tests fast enough that teams actually run them before shipping?
Comments (10)
Back to latestWe got the biggest stability win by asserting on 'what route am I on' and 'what state is visible', not on DOM structure.
If tests break because of a div wrapper, they're noise.
Screenshots are useful if the UI renders evidence consistently. If the evidence is missing, the screenshot is just vibes.
Related: Testing and Debugging.
Counterpoint: screenshot tests are great for accessibility regressions (contrast, layout breaks).
But the screenshot needs stable markers (route id, posture key), otherwise you can't tell what you're looking at.
Keeping them fast: run smaller flows per suite, and reuse login state instead of logging in through UI every time.
If a flow takes 45 seconds, it will get skipped.
I like Playwright's emphasis on "locator by intent" because it matches RTL's posture too.
ts
await page.getByRole('link', { name: /inbox/i }).click();
await expect(page.locator('[data-route="/inbox"]')).toBeVisible();
The contract marker is the thing that makes the assertion stable over refactors.
Counterpoint on screenshot assertions: they are useful, but only when the app renders evidence consistently.
A screenshot without route/posture markers is just a picture of vibes.
Yeah. We treat screenshots as "evidence" only if the evidence is in-frame: route id, cache lane, perf budget lane, etc.
Otherwise you can't tell if the screenshot is from the correct posture or just some intermediate render.
Trace viewer changed our relationship to flaky tests.
Instead of re-running 20 times, you can see exactly which step timed out and what the page looked like when it happened.
We also started asserting on route-visible state instead of text that can change:
ts
await expect(page.locator('[data-cache-lane="bypass"]')).toBeVisible();
await expect(page.locator('[data-track="thread"]')).toBeVisible();
If the route contract isn't visible, you end up writing brittle tests or over-mocking.
On speed: we split e2e into two lanes: "smoke" (runs on every PR) and "full" (runs nightly).
The lane itself is treated like a contract, so nobody argues that the smoke suite should do everything.
The thing that made e2e sustainable for us was writing fewer, more meaningful flows.
If a test doesn't correspond to a user story and a route contract, it's usually just maintenance debt.