Reliable browser automation comes from synchronizing with application state, using locators that describe the intended control, isolating every test, and asserting the result with built-in waiting. Fixed delays and selectors coupled to a page’s internal DOM make suites slow and fragile. The practices below apply to Selenium and Playwright and give you a repeatable way to diagnose failures instead of hiding them.
1. Define reliability before writing a test
A dependable test has a clear precondition, one user-level action, and an observable outcome. Write those three parts before choosing selectors or waits:
- Precondition: the account, data, route, and permissions the test needs.
- Action: a click, form submission, navigation, or other user operation.
- Outcome: a visible message, URL, table row, download, or state change that proves the action worked.
Keep each test independent. Playwright recommends isolating cookies, storage, and test data so one failure cannot contaminate another. Selenium suites benefit from the same rule: create or reset the required data in setup, and clean up when the test ends. Shared accounts and order-dependent tests turn an otherwise passing suite into a chain of cascading failures.
2. Synchronize on conditions, not guessed delays
Modern pages continue rendering after the initial document load. JavaScript may fetch data, hydrate components, animate a control, or replace a button after your command starts. Selenium calls race conditions between application readiness and automation commands “one of the primary causes of flaky tests” in its Waiting Strategies guide.
#1 Best Overall
Why fixed sleeps fail
A sleep is either too short for a slow run or unnecessarily long for a fast one. It also says nothing about whether the page reached the state your next command requires. A five-second pause cannot prove that a particular request completed or that a button is enabled.
Use explicit, condition-specific waits in Selenium
Wait for the condition needed by the next operation: visibility before reading, clickability before clicking, a URL before continuing, or a specific element before asserting.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
browser = webdriver.Chrome()
wait = WebDriverWait(browser, 15)
try:
browser.get("https://example.com/checkout")
email = wait.until(EC.visibility_of_element_located((By.ID, "email")))
email.send_keys("[email protected]")
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']"))).click()
confirmation = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "[role='status']")))
assert "complete" in confirmation.text.lower()
finally:
browser.quit()
Do not mix implicit waits with carefully tuned explicit waits without understanding the interaction; compounded polling can make failures slow and timing opaque. A larger timeout is not a fix for an incorrect locator or an outcome that never occurs.
Let Playwright wait for actionability
Playwright locator actions check conditions such as visibility, stability, enabled state, and the ability to receive events before acting. Its actionability model is documented in Auto-waiting. Prefer this built-in synchronization over sleeps:
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteimport { test, expect } from '@playwright/test';
test('submits checkout', async ({ page }) => {
await page.goto('https://example.com/checkout');
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByRole('status')).toContainText('complete');
});
3. Choose locators that survive UI change
A locator is a contract between the test and the interface. It should identify one intended control and remain understandable to the next engineer.
Rank #2
Playwright’s user-facing preference
Use accessible roles, labels, visible text, placeholders, or an explicit test ID that the application treats as a deliberate contract. The Playwright locator guide warns that long CSS and XPath chains tied to DOM structure are fragile. Avoid using .first() or .nth() merely to silence an ambiguous match; make the locator more precise.
await page.getByRole('dialog', { name: 'Delete account' })
.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Account deleted')).toBeVisible();
Selenium’s stable-ID emphasis
Selenium’s locator guidance prefers a unique, predictable HTML ID when one exists. Otherwise use a compact, readable selector. Avoid traversing several parent and sibling levels just to reach a control.
from selenium.webdriver.common.by import By
# Prefer a unique contract supplied by the page:
submit = driver.find_element(By.ID, "checkout-submit")
# A concise fallback can be appropriate:
error = driver.find_element(By.CSS_SELECTOR, "[role='alert']")
There is no universal “best selector.” Match the framework and the application’s contract, and make ambiguity fail loudly during development rather than silently selecting an unintended element.
4. Assert the user-visible result
Checking that a command returned is weaker than checking what the user can see. Asynchronous interfaces often update after the click call completes. Playwright web-first assertions retry until the expected condition is met; a one-time visibility read does not.
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('status')).toHaveText('Saved');
await expect(page).toHaveURL(//settings/profile/);
In Selenium, put the same expectation behind an explicit wait:
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "[role='status']"), "Saved"
))
assert "/settings/profile" in driver.current_url
Assert the effect that matters: a confirmation, changed URL, new row, enabled control, or downloaded artifact. Avoid asserting implementation details such as a particular wrapper class unless that class is the intentional contract.
5. Isolate data, storage, and browser context
Give every test the state it needs. A fresh Playwright context provides separate cookies and storage:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
import { test } from '@playwright/test';
test('uses isolated state', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
// Create or seed only the data this test needs.
await context.close();
});
For Selenium, create a new driver when practical, or explicitly clear cookies and reset server-side fixtures between tests. Parallel execution requires unique usernames, IDs, files, and ports; otherwise tests can be individually correct but fail through resource collisions.
6. Build a diagnostic loop for failures
Confirm the locator
Check how many elements match and whether the match is the intended control. Playwright’s Inspector and VS Code integration show live locator matches and actionability logs. Use them to see whether an element is hidden, disabled, moving, covered, or duplicated. These tools are described in Playwright best practices.
Capture evidence
On failure, retain the URL, console errors, network failures, screenshot, and (where supported) a trace or browser log. Evidence distinguishes an application defect from a stale selector, blocked request, authentication expiry, or environment problem.
Do not mask the cause
A force-click bypasses actionability checks and can produce a test that passes without representing a real user action. Replacing a failing step with a longer sleep has the same problem. First determine which condition is missing; then wait for that condition or fix the application contract.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →7. Make the suite fast without making it vague
- Reuse a browser process where the framework supports it, but create isolated contexts or sessions per test.
- Wait for the smallest meaningful condition rather than global network-idle assumptions when the page legitimately keeps connections open.
- Use API or fixture setup for data creation when the behavior under test is not the data-entry flow.
- Run independent tests in parallel only after removing shared state.
- Set a bounded timeout and report the condition that timed out, not just “test failed.”
Record retries separately from first-pass results. A retry can help classify transient infrastructure failures, but it should not turn a consistently broken assertion into a green build.
8. Compare frameworks and execution environments
Choose based on the whole team and pipeline rather than a claim that one framework eliminates flakiness.
| Decision axis | Selenium | Playwright |
|---|---|---|
| Language and ecosystem | Useful when your organization already has WebDriver-based libraries, bindings, and infrastructure. | Strong fit when the team wants its locator API, auto-waiting actions, and web-first assertions. |
| Synchronization | Explicit waits for named conditions are central to reliable tests. | Locator actions and assertions provide built-in waiting and retrying. |
| Locators | Prefer unique IDs, then compact readable selectors. | Prefer roles, labels, text, placeholders, or deliberate test IDs. |
| Debugging | Use browser logs, screenshots, and your test runner’s diagnostics. | Inspector, VS Code locator inspection, and actionability logs are documented tools. |
| Coverage and infrastructure | Evaluate the browsers, devices, grid, and language bindings your existing CI requires. | Evaluate its supported browser/device targets and how its contexts and traces fit CI. |
Both can be run locally or through a hosted cross-browser service. BrowserStack’s official materials describe support for Selenium and Playwright and testing across browsers and devices; whether that is worthwhile depends on your required coverage and existing infrastructure. See its support documentation and pricing page for current details.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.9. A practical reliability checklist
- Replace every arbitrary sleep with a condition tied to the next action or assertion.
- Make each locator unique, readable, and based on a documented UI contract.
- Use retrying assertions for asynchronous outcomes.
- Reset cookies, storage, accounts, and server data between tests.
- Keep test data and temporary files unique under parallel execution.
- Capture traces, screenshots, logs, and URLs when a test fails.
- Investigate ambiguity or actionability failures before using force options.
- Compare frameworks by language, browser coverage, synchronization, debugging, and CI fit.
Or skip the browser setup
If your requirement is a reliable image or PDF of a page rather than an interactive test, ScreenshotNeo provides a website screenshot API and MCP server. A single request can return PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Use the API with the same URL and parameters documented at ScreenshotNeo’s documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Its Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Should I use a global timeout for every test?
Use a bounded default as a safety net, then set condition-specific waits where a particular operation legitimately takes longer. A global increase cannot correct a wrong locator or missing application state.
When is a test ID better than a role or label?
Use a test ID when the interface has no stable accessible name or when the team deliberately defines a non-visual contract. Keep it short and unique, and treat changes to it as an intentional test-contract change.
Recommended Free Tools
Do retries make flaky tests reliable?
Retries can expose transient infrastructure problems, but they do not repair synchronization, locator, or state-isolation defects. Track retry frequency and investigate any test that needs retries repeatedly.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

