Playwright Tips and Tricks

Playwright Tips and Tricks
Playwright is one of the most powerful tools for end-to-end testing, it’s currently taking over the scene with its speed and reliability. However, writing tests that are both fast and reliable can be tricky. And if not done well, playwright tests become more of a technical debt and burden more than a tool to verify product quality.
I’ve compiled some of my most used tricks in the past two years of using Playwright, and I hope they help you write better tests.
⌛ Avoid waitForLoadState('networkidle')
– You Definitely Waited Too Much
You might be tempted to use this when loading your test page because it guarantees that the page and all API calls are
done loading. networkidle
waits for a moment when there are no network requests, which might never come for apps that continuously
poll or load analytics. Playwright docs strongly discourage this.
Use more specific criteria, like waitForSelector
on a key UI element, to reduce flakiness and
improve speed.
✅ Don’t use waitForTimeout
– You’ll Never Wait Just Right
Sometimes you’re waiting for an animation to finish or interaction to complete. You might think waitForTimeout
is the
answer, but it’s not. Your tests will run on different machines and sometimes in CI environments, with different
processing powers and different network speeds.
And while timeouts might seem like a good idea locally, when run by different environments, you’ll start seeing flaky tests. Playwright docs also discourage this.
Prefer built-in waiting mechanisms that wait for conditions like visibility or element stability. For example, use
waitForSelector
or waitForFunction
to wait for specific conditions instead of arbitrary timeouts.
🔎 Use Playwright’s Built-in Assertions - The biggest Flaky Test Killer (edited)
While in the past I used to do things like
const text = await page.getByRole('button').textContent();
expect(text).toBe('Submit');
This is very prone to flakiness if the text is not there yet, since there is no default retrial and waiting mechanism.
Playwright exposes its own set of assertions that are built on top of the expect
library. They wait for the condition to be
true before proceeding. For example, you can do:
await expect(page.getByRole('button')).toHaveText('Submit');
Check out the entire list of Playwright assertions for more details.
⚔️ Use Promise.all([...])
for Racing Conditions
Sometimes you’re triggering one action and then waiting for another to finish. For example, you might be clicking a button and then waiting for a navigation event. If you do this sequentially, you might run into a race condition where the navigation happens before the click event is fully processed.
Other times, you might be waiting for a network request to finish before clicking a button. This can lead to flaky tests if the network request takes longer than expected.
Use Promise.all([...])
to handle these situations. For example, if you’re clicking a button and waiting for a
navigation event, do it like this:
await Promise.all([
page.waitForNavigation(),
page.click('text=Submit'),
]);
This ensures that both actions are handled concurrently, reducing the chance for a wait that never fully completes.
🧑🦯 Prefer Human-Readable and Accessible Selectors
Selectors like [data-testid="submit-button"]
or role=button[name="Submit"]
make tests more robust and readable.
Avoid brittle CSS chains like .button-wrapper > button:nth-child(2)
which easily break with UI changes. Leverage
Playwright’s accessibility selectors whenever possible.
🧪 Shard and Run in Parallel — Then Merge Reports
Speed up test suites by sharding tests across CI nodes. Use Playwright’s built-in support to run in parallel. Playwright
also supports outputting reports in blob format that you can later in a next step merge using playwright merge-report
.
This is especially useful for large test suites or when you have slow tests. Giving an overall better DevXperience.
🐢 Use test.slow()
for Intentionally Slower Tests
If a test interacts with large datasets, animations, or intentionally slow workflows, annotate it with test.slow()
to adjust timeouts and avoid false negatives in CI. This also documents why a test takes longer.
🚫 Use test.skip()
for Platform/Env Specific Tests
When a test doesn’t apply to a certain browser, OS, or environment, use test.skip()
or test.skip(condition, reason)
. This keeps the test suite green and understandable without removing valuable tests:
test.skip(({ browserName, isMobile }) => {
return browserName === 'firefox' && isMobile;
}, 'Firefox mobile is not supported');
🔀 Integrate msw
(Mock Service Worker) — But Only for Unstable APIs
While end-to-end tests usually require using real APIs sometimes you run into situations where you need a certain state in the APIs that isn’t achievable easily. For instance when you want to mock an infinite loading state or an API failure to test how your app behaves in those situations.
In those cases, you can use msw
to mock the API responses. I found this library playwright-msw to work really well in integrating Playwright with msw
Throughout my last two years of using Playwright, I’ve seen times when I had to debug flaky tests for hours, and times when I was able to write tests that were fast and reliable. The difference was always in the patterns I used.
By following these best practices, you can write tests that are not only fast and reliable but also easy to maintain and read. Hope this was helpful, and happy testing!