UI Automation
Driving a real browser: Selenium and Playwright, page objects, and the discipline that keeps tests from going flaky.
The Expensive Layer
Everything in a browser is asynchronous, so most UI test failures are timing rather than logic. The craft is expressing intent in a way that survives a redesign, and waiting for the right condition rather than for a number of seconds.
Selenium
Selenium WebDriver is the long-standing standard: a W3C protocol, every major language binding, every browser, and the automation layer underneath a great deal of enterprise tooling.
| Strength | Cost |
|---|---|
| Universal browser and language support | Waiting is your problem to solve |
| A genuine standard, not one vendor | More setup — drivers, versions, grid |
| Huge ecosystem and hiring pool | Slower feedback loop than modern tools |
| Selenium Grid for parallel runs | Infrastructure to maintain |
sleep(3) is the single biggest source of flaky Selenium suites. It is too short on a loaded CI machine and wasted time everywhere else. Use explicit waits for a condition — element visible, request finished, text present.Playwright
Playwright takes the newer approach: one API across Chromium, Firefox and WebKit, with auto-waiting built into every action. Clicking waits for the element to exist, be visible, be stable and be enabled — which removes most of the wait code a Selenium suite carries.
// auto-waits for the button, and for the assertion to become true
await page.getByRole('button', { name: 'Check out' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
// intercept the network instead of depending on a live backend
await page.route('**/api/prices', route =>
route.fulfill({ json: { total: 4200 } })
);
| Feature | Why it matters |
|---|---|
| Auto-waiting | Removes the main cause of flakiness |
| Trace viewer | A failed CI run replays with DOM snapshots |
| Network interception | Test the UI without depending on a backend |
| Parallel isolated contexts | Fast, and no state leaking between tests |
Locator strategy matters more than the tool. Prefer role and accessible name — getByRole('button', { name: 'Save' }) — then test ids. CSS paths tied to layout break on the next redesign, and XPath through five divs breaks on the one after.
Page Object Model
Without structure, every test contains selectors, and a renamed field means editing forty files. The page object model puts the selectors and interactions for one screen behind a small class, so tests read as intent.
class CheckoutPage {
constructor(page) { this.page = page; }
async payWith(card) {
await this.page.getByLabel('Card number').fill(card.number);
await this.page.getByRole('button', { name: 'Pay' }).click();
}
confirmation() { return this.page.getByText('Order confirmed'); }
}
// the test says what, the page object knows how
await checkout.payWith(validCard);
await expect(checkout.confirmation()).toBeVisible();
Fixing Flaky Tests
A flaky test passes and fails on the same code. It is worse than a failing test, because it teaches the team that red means nothing.
| Cause | Fix |
|---|---|
| Fixed sleeps | Wait for a condition, not a duration |
| Shared test accounts or data | Create fresh data per test |
| Test order dependence | Reset state; run in random order to expose it |
| Animations and transitions | Disable them in the test environment |
| Live third-party services | Intercept the network and serve a fixture |
| Race on load | Assert on the settled state, not an intermediate one |
Interview Questions
Why are UI tests kept few?
They are the slowest and most fragile layer. They prove a journey works end to end, which nothing else does, but a large UI suite is slow, flaky and eventually ignored.
Selenium or Playwright?
Selenium is the W3C standard with the widest language and browser reach and a huge ecosystem. Playwright auto-waits on every action, intercepts network and ships a trace viewer, which removes most flakiness by default.
What is the best locator strategy?
Role and accessible name first, then dedicated test ids. CSS paths and XPath tied to structure break on the next redesign, and they also say nothing about intent.
What does the page object model buy you?
One place per screen holding selectors and interactions, so tests express intent and a renamed field is a single edit rather than forty.
Why is a flaky test worse than a failing one?
It destroys trust in the whole suite. Once red sometimes means nothing, people rerun until green and real failures pass through unnoticed.
Why not auto-retry flaky tests?
A retry hides the cause, which is sometimes a genuine race condition in the product. Quarantine, diagnose and fix, then return the test to the suite.