UI tests have a reputation for being brittle. They fail when a button moves two pixels. They timeout when the network is slow. They take forever to run. But it doesn't have to be this way. Here are principles for designing UI tests that earn their keep.

1. Test Behavior, Not Implementation

Don't couple your tests to CSS classes, DOM structure, or implementation details. Couple them to user-visible behavior.

// Fragile: breaks when class names change
await page.click('.btn-primary.submit-form');

// Better: use role, label, or test ID that reflects intent
await page.click('button[type="submit"]');
// or: getByRole('button', { name: 'Submit' })

If the user doesn't care about a class name, your test shouldn't either.

2. Use Stable Selectors

Prefer semantic attributes over layout:

  • role and aria-label for accessibility-minded selectors
  • data-testid when you need a hook and nothing else fits
  • Avoid: nth-child, generic class names, long XPath chains

When you add a data-testid, you're making a contract: "This element is important for testing." That's okay. Just keep the IDs meaningful and stable.

3. Wait for Real Conditions

Don't rely on fixed sleep() calls. Wait for the state you actually care about:

  • Element visible and stable
  • Network request completed
  • Animation finished

Most test frameworks provide waitFor-style APIs. Use them. A test that sleeps for 3 seconds is either slow or still flaky.

4. Isolate Tests

Each test should be independent. Don't assume a previous test left the system in a known state. Use fixtures, setup/teardown, or a fresh session. Shared state is the enemy of reliable tests.

5. Keep the Feedback Loop Tight

If your UI suite takes 30 minutes, people will stop running it. Prioritize:

  • A small smoke suite that runs in minutes
  • Parallelization for the full suite
  • Clear failure messages so you know what broke, not just that it broke

Summary

Reliable UI tests are possible. They require discipline: stable selectors, behavioral focus, proper waiting, isolation, and a fast feedback loop. Invest in these, and your UI tests will start to feel like assets instead of liabilities.