This guide walks you through writing browser tests that run reliably across Chrome, Firefox, Safari, and Edge. You’ll learn how to structure tests, choose robust selectors, handle timing, and avoid flaky failures—so your test suite gives you confidence instead of false alarms.
What You’ll Need
- A test framework: Playwright, Cypress, or Selenium WebDriver (this guide uses generic concepts that apply to all).
- Access to at least two browsers (e.g., Chrome and Firefox) for local verification.
- A basic understanding of JavaScript or your chosen language’s test syntax.
- Optional: a CI service (like GitHub Actions or Jenkins) to run tests automatically.
Step-by-Step
- Start with a single source of truth for test data. Define constants for URLs, user credentials, and expected values in a separate file. Never hardcode these inside test cases—when the app changes, you update one place instead of hunting through dozens of tests.
- Use semantic, stable selectors. Prefer
data-testidattributes or ARIA roles over CSS classes or XPath that depend on DOM structure. For example,getByRole('button', { name: 'Submit' })is more robust thanbutton.btn-primary. If your team doesn’t use test IDs yet, add them to the most critical elements. - Wait for the right thing, not just time. Never use fixed sleeps like
wait(3000). Instead, wait for the element to be actionable (visible, enabled, stable) or for a network response to complete. Most modern frameworks have built-in auto-waiting—use it. - Isolate tests from each other. Each test should set up its own state (e.g., create a fresh user or reset the database) and clean up after itself. Avoid relying on a specific order—tests should pass when run individually or in any sequence.
- Run tests headlessly and in headed mode. Headless catches most issues, but some layout bugs only appear in a real window. Run both locally and in CI to cover both scenarios.
- Use a consistent viewport size. Set the viewport explicitly in your test configuration (e.g., 1280×720). Different default window sizes can cause elements to shift or hide, leading to false failures.
- Handle animations and transitions. Disable CSS animations in test mode if possible, or wait for them to finish. A common pattern is to add a class like
disable-animationsto the body in test environments. - Mock external services. If your app calls third-party APIs, mock them with stubs. This makes tests fast, deterministic, and independent of network conditions. Keep a few end-to-end tests that hit real services to verify integration, but run them separately and mark them as ‘smoke’.
- Write tests at the right level. Use unit tests for pure logic, component tests for UI behavior, and end-to-end tests only for critical user journeys. Over-testing through the UI slows down your suite and increases flakiness.
- Use parallelization wisely. Run tests in parallel to save time, but be careful with shared state (like a shared database). Use isolated environments or unique data per test to avoid collisions.
- Add retries for known flaky areas. Some tests may fail due to network latency or third-party widgets. Instead of ignoring failures, add a retry mechanism (e.g., Playwright’s
retriesoption) but keep it low (1-2 attempts) and investigate the root cause. - Keep tests short and focused. Each test should verify one behavior. If a test has many assertions, split it. This makes failures easier to diagnose and reduces the chance of one error cascading.
Common Mistakes
- Using CSS selectors that depend on layout. Classes like
.header .nav-item:nth-child(2)break when the design changes. Use data attributes instead. - Testing implementation details. Asserting on a loading spinner’s class name instead of the final content leads to brittle tests. Test what the user sees, not how it’s built.
- Ignoring mobile viewports. If you only test desktop, you miss responsive breakpoints. Run a few tests on mobile-sized viewports.
- Hardcoding URLs with environment-specific ports. Use a base URL from configuration, not
http://localhost:3000everywhere. - Not cleaning up test data. Leftover data can cause false failures in later runs. Always clean up after each test.
- Over-relying on screenshots. Visual regression tests are useful, but they can be flaky due to font rendering differences across OSes. Use them sparingly and with a pixel tolerance.
Tips
- Use a linter for your tests. Tools like ESLint with the Playwright plugin catch common mistakes like using
page.waitForTimeout. - Name tests clearly. A test named ‘login works’ is less helpful than ‘logs in with valid credentials and sees dashboard’. Good names make failures easier to triage.
- Run tests on every pull request. The earlier you catch a regression, the cheaper it is to fix.
- Use a test runner that supports sharding. Splitting tests across multiple CI machines speeds up execution.
- Record and watch test recordings. Most modern frameworks record videos of failed tests—use them to see what happened.
Troubleshooting
Test passes locally but fails in CI. Common causes: different screen resolution, missing system fonts, or slower network. Set the viewport explicitly in CI, install the same fonts, and increase timeouts for network-heavy operations.
Element not found intermittently. The element may be rendered after a delay. Instead of increasing timeout globally, wait for the specific element to be visible. Also check if the element is inside an iframe—you may need to switch frames.
Test fails only in Safari. Safari handles some CSS and events differently (e.g., input[type=date] formatting). Use feature detection or adjust the test to be browser-agnostic. If it’s a real bug, report it to the dev team.
Flaky tests due to animations. Disable animations in test mode via a global CSS class or use the browser’s ‘prefers-reduced-motion’ setting.
Parallel tests interfere with each other. If tests share a database, use unique data per test (e.g., timestamp in usernames) or run them serially. Alternatively, spin up isolated environments.
Tests run too slowly. Identify slow tests by profiling. Often they wait for unnecessary timeouts or make excessive network calls. Mock more services or refactor to smaller tests.
Frequently Asked Questions
Why do my tests pass in Chrome but fail in Firefox?
Different browsers may have different default font sizes, scrollbar widths, or event timing. Use a consistent viewport and disable animations to reduce differences. Also, check if you’re relying on browser-specific behavior like window.scrollTo with smooth behavior.
Should I use CSS selectors or XPath?
Avoid both when possible. Use data-testid or ARIA roles. If you must use them, prefer CSS for readability, and only use XPath for complex traversals (rarely needed).
How do I handle file uploads in cross-browser tests?
Use the framework’s built-in setInputFiles method instead of simulating keyboard input. It works consistently across browsers.
What’s the best way to wait for a page to load?
Wait for a specific element that indicates the page is ready, not the load event. For SPAs, wait for the content you expect to see. Avoid waitForTimeout entirely.
Can I test responsive design in a single browser?
Yes, by setting the viewport size. But you should still test on real devices or at least on a few different viewport widths to catch layout issues.
How do I reduce flaky tests without ignoring them?
First, identify the root cause: is it a timing issue, a selector problem, or a test isolation problem? Fix the cause, not the symptom. Add retries only as a temporary measure while you investigate.
Related
General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.
More in this bed
- Google Chrome Helper: What It Is & How to Disable
- Fix 0x80070422 on Windows Server (Step-by-Step)
- How to Troubleshoot a Malfunctioning Driver on Windows
- How to Create Folders in Microsoft Outlook
- Fix Status Range List Conflict 0xc0000282 in Windows
- Fix Software Not Uninstalling on Windows 10