Skip to content
137 changes: 137 additions & 0 deletions docs-site/docs/why/why-accessibility-first-locators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
description: "Why accessibility-first locators: targeting the accessibility tree (getByRole, getByLabel, getByText) survives CSS rewrites and markup refactors without hiding Selenium."
id: why-accessibility-first-locators
title: Why accessibility-first locators?
sidebar_label: Why accessibility-first locators?
sidebar_position: 4
---

# Why accessibility-first locators?

Automated tests break for two reasons: real regressions, and brittle selectors. The second kind is wasted time.

Most Selenium test suites are packed with CSS selectors and XPath expressions tied directly to DOM hierarchies or styling hooks:

```java
// Brittle: bound to exact tag hierarchy and volatile utility classes
driver.findElement(By.cssSelector("form.auth-form div.input-group > input.email-input"))
.sendKeys("user@example.com");
driver.findElement(By.cssSelector("form.auth-form div.actions button.btn-primary"))
.click();
```

When frontend developers restructure a component, migrate CSS frameworks (e.g. from Bootstrap to Tailwind), or wrap a field in an extra `<div>`, the application still works perfectly for end users. But the test suite turns red.

---

## What accessibility-first locators do

Users and assistive technologies (like screen readers) do not interact with CSS class names or DOM parent-child nesting. They interact with **accessible roles** and **names**:

- A button labeled **"Sign In"**
- A text input labeled **"Email address"**
- A heading at **Level 1** reading **"Dashboard"**

Selenium Boot introduces accessibility-first locators (`getByRole`, `getByLabel`, `getByText`, `getByPlaceholder`, `getByTestId`) available directly in `BaseTest` and `BasePage`:

```java
// Resilient: targets what the user sees and perceives
getByLabel("Email address").type("user@example.com");
getByRole(Role.BUTTON).withName("Sign In").click();
```

---

## Surviving real markup refactors

Consider a button that goes through common frontend iterations across three sprints:

```html
<!-- Sprint 1: Native button styled with CSS class -->
<button class="btn btn-primary">Sign In</button>

<!-- Sprint 2: Replaced with styled anchor for design system requirements -->
<a href="#" role="button" class="inline-flex items-center px-4 py-2 bg-blue-600 rounded">Sign In</a>

<!-- Sprint 3: Form converted to standard HTML input -->
<input type="submit" value="Sign In" class="cta-submit" />
```

Here is how different locator strategies hold up:

| Locator Strategy | Sprint 1 | Sprint 2 | Sprint 3 | Why |
|---|---|---|---|---|
| `By.cssSelector("button.btn-primary")` | Pass | **Fail** | **Fail** | Tag changed to `<a>`, class changed |
| `By.xpath("//button[text()='Sign In']")` | Pass | **Fail** | **Fail** | `button` tag is gone |
| `getByRole(Role.BUTTON).withName("Sign In")` | **Pass** | **Pass** | **Pass** | All 3 expose the accessible role `button` and name `"Sign In"` |

`Role.BUTTON` targets both native HTML elements with implicit roles (`<button>`, `<input type="submit">`, `<summary>`) and any element with an explicit `role="button"`. The accessible name calculation follows standard W3C ARIA precedence (`aria-label` → `aria-labelledby` → `<label>` → text content → `value`). For Sprint 3's `<input type="submit">`, `Role.BUTTON` matches the native input type, and the accessible name computation falls back to the `value` attribute (`"Sign In"`), keeping the locator passing across all three implementations.

---

## Before & after

### Form filling

**Plain Selenium (brittle selectors + explicit wait boilerplate):**

```java
// Fragile against label ID changes or container restructuring
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement emailInput = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[@id='user-fields']//input[1]"))
);
emailInput.sendKeys("alex@example.com");

WebElement submitBtn = wait.until(
ExpectedConditions.elementToBeClickable(By.cssSelector("div.dialog-actions > button.submit-btn"))
);
submitBtn.click();
```

**Selenium Boot (accessibility-first + auto-waiting):**

```java
// Reads like user intent, survives markup restructuring
getByLabel("Email address").type("alex@example.com");
getByRole(Role.BUTTON).withName("Submit").click();
```

---

## …without hiding Selenium

Accessibility-first locators do not hide `WebDriver` behind a restrictive proprietary abstraction:

1. **Transparent Selenium By**: Every semantic locator can produce a standard Selenium `By` instance using `.toBy()`:
```java
By buttonBy = Locator.byRole(Role.BUTTON).withName("Submit").toBy();
WebElement rawElement = getDriver().findElement(buttonBy); // or driver.findElement(...) in BasePage
```
2. **Mix and match**: Use `getByRole` for UI actions and drop down to `getDriver().findElement(By.id("canvas"))` whenever you need low-level WebDriver control.
3. **Chainable auto-waiting**: Locators return `Locator` objects that poll up to `timeouts.explicit` and automatically re-resolve stale DOM elements, eliminating `StaleElementReferenceException` without extra code.

---

## Locator comparison summary

| Feature | CSS / XPath | Accessibility-First (`getBy*`) |
|---|---|---|
| Target | DOM structure / CSS classes | Accessibility tree (Role + Name) |
| Resilience to CSS class renames | Low (coupled to utility/styling classes) | High (uncoupled from styling classes) |
| Resilience to semantic tag refactors | Low (hardcoded tag names break) | High (matches implicit and explicit ARIA roles) |
| Auto-waiting on action (`click()`, `type()`) | No | **Yes** |
| Re-resolves stale DOM references | No | **Yes** |
| Escape hatch to raw `By` | Native | `.toBy()` |

*(Note: While accessibility-first locators decouple tests from styling classes and DOM nesting, they remain focused on the user contract — tests will naturally require updates if a redesign fundamentally removes or alters the accessible name or role.)*

---

## Next steps

- [Accessibility-First Locators Guide](/docs/guides/semantic-locators) — complete `getByRole`, `getByLabel`, `getByText` reference
- [Smart Locator](/docs/guides/smart-locator) — the chainable `$()` API
- [Why WaitEngine?](/docs/why/why-wait-engine) — centralized waiting vs scattered `WebDriverWait`
- [Why Selenium Boot?](/docs/why/why-selenium-boot) — the overall philosophy

2 changes: 2 additions & 0 deletions docs-site/docs/why/why-not-plain-selenium.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,7 @@ Because it *is* Selenium, adoption is incremental — you can point a single tes
## Next steps

- [Migrate from Selenium + TestNG](/docs/migration/from-selenium-testng) — the side-by-side "delete the plumbing" guide
- [Why accessibility-first locators?](/docs/why/why-accessibility-first-locators) — how semantic locators survive markup changes
- [Why WaitEngine?](/docs/why/why-wait-engine) — centralized auto-waiting vs scattered WebDriverWait
- [Why Selenium Boot?](/docs/why/why-selenium-boot) — the overall philosophy
- [Getting Started](/docs/getting-started) — first test in 5 minutes
2 changes: 2 additions & 0 deletions docs-site/docs/why/why-not-playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ Both are legitimate. Selenium Boot exists so that "we're a Selenium shop" no lon

- [Coming from Playwright](/docs/migration/coming-from-playwright) — the full bridge page
- [Accessibility-First Locators](/docs/guides/semantic-locators) — the `getByRole`/`getByLabel` family
- [Why accessibility-first locators?](/docs/why/why-accessibility-first-locators) — how semantic locators survive markup changes
- [Why WaitEngine?](/docs/why/why-wait-engine) — centralized auto-waiting vs scattered WebDriverWait
- [Why Selenium Boot?](/docs/why/why-selenium-boot) — the overall philosophy
4 changes: 3 additions & 1 deletion docs-site/docs/why/why-selenium-boot.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ Outcomes first — with the API that delivers them:
- **Teams already invested in Selenium** who want Playwright-style ergonomics without leaving the Selenium / Java / TestNG stack, their team's skills, or Selenium Grid.
- **Teams maintaining a home-grown framework** — a `BaseTest`, a `DriverFactory`, a pile of wait utilities — who'd rather delete that plumbing than keep debugging it.

The two "why" questions those teams actually ask each have their own page:
The "why" questions those teams actually ask each have their own page:

- [Why not plain Selenium?](/docs/why/why-not-plain-selenium) — what the boilerplate really costs
- [Why not Playwright?](/docs/why/why-not-playwright) — an honest comparison (we don't claim to replace it)
- [Why accessibility-first locators?](/docs/why/why-accessibility-first-locators) — surviving markup and styling changes
- [Why WaitEngine?](/docs/why/why-wait-engine) — centralized auto-waiting vs scattered WebDriverWait

---

Expand Down
148 changes: 148 additions & 0 deletions docs-site/docs/why/why-wait-engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
description: "Why WaitEngine: centralized auto-waiting and fluent static waits replace scattered WebDriverWait boilerplate and flaky Thread.sleep without hiding Selenium."
id: why-wait-engine
title: Why WaitEngine?
sidebar_label: Why WaitEngine?
sidebar_position: 5
---

# Why WaitEngine?

Timing is the number one cause of flaky Selenium tests. Most teams try to solve it with scattered `WebDriverWait` boilerplate or succumb to `Thread.sleep`.

Plain Selenium provides the primitives — `WebDriverWait` and `ExpectedConditions` — but leaves the implementation and lifecycle entirely up to you. Over time, every test suite develops the same set of issues.

---

## The two synchronization traps

Without a centralized wait strategy, test suites invariably fall into two traps:

### 1. Scattered `WebDriverWait`
Every page object and test method instantiates its own wait:
```java
// Page A
new WebDriverWait(driver, Duration.ofSeconds(5)).until(...);

// Page B
new WebDriverWait(driver, Duration.ofSeconds(10)).until(...);

// Utility class
new WebDriverWait(driver, Duration.ofSeconds(30)).until(...);
```
Timeouts become arbitrary and inconsistent across tests. If a slower staging environment requires increasing explicit timeouts from 5 seconds to 10 seconds, developers must touch dozens of files or build bespoke wrapper classes.

### 2. Flaky `Thread.sleep`
When explicit waits feel tedious or when modern JavaScript apps re-render components mid-interaction, engineers insert blind sleeps:
```java
Thread.sleep(2000); // "Just to be safe"
```
A 2-second sleep runs for 2 seconds even when the element was ready in 50 milliseconds. Multiplied across hundreds of tests, build times balloon. Worse, under heavy CI server load, that 2-second sleep is often not enough, and the test flakes anyway.

---

## Centralized auto-waiting with WaitEngine

`WaitEngine` centralizes explicit waits behind a clean, static API powered by a single configuration key: `timeouts.explicit` in `selenium-boot.yml` (default: 5 seconds).

```yaml title="selenium-boot.yml"
timeouts:
explicit: 5
```

When you need an explicit synchronization point, call `WaitEngine` directly without instantiating wait objects or passing `WebDriver` around:

```java
import com.seleniumboot.wait.WaitEngine;

// Clean, static synchronization driven by selenium-boot.yml
WaitEngine.waitForClickable(By.id("checkout-btn")).click();
```

And when using Selenium Boot's locators (`$()`, `getByRole()`, `getByLabel()`), explicit waits disappear entirely: locator actions **auto-wait** up to `timeouts.explicit` by default.

---

## Before & after

### Synchronizing dynamic elements

**Plain Selenium (verbose instantiation + potential stale reference):**

```java
// Manual WebDriverWait setup with hardcoded timeout
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement saveBtn = wait.until(
ExpectedConditions.elementToBeClickable(By.id("save-settings"))
);
saveBtn.click(); // Can throw StaleElementReferenceException if the DOM re-rendered

// Blind sleep often added to await asynchronous background saves
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {}
```

**Selenium Boot (WaitEngine + auto-waiting locators):**

```java
// WaitEngine re-fetches elements on poll and respects centralized config:
WaitEngine.waitForClickable(By.id("save-settings")).click();

// Or with Locator — zero explicit wait code needed:
$("#save-settings").click();
```

---

## Out of the box condition coverage

`WaitEngine` provides pre-built conditions for real-world web scenarios without requiring custom `ExpectedCondition` implementations:

| Method | What it waits for |
|---|---|
| `WaitEngine.waitForVisible(By)` | Element is present in DOM and displayed |
| `WaitEngine.waitForInvisible(By)` | Loading spinners, modals, or overlays to disappear |
| `WaitEngine.waitForClickable(By)` | Element is visible and enabled for interaction |
| `WaitEngine.waitForText(By, text)` | Element displays expected visible text |
| `WaitEngine.waitForTextMatches(By, regex)` | Element text matches a regular expression pattern |
| `WaitEngine.waitForUrlMatches(regex)` | Current browser URL matches a regex |
| `WaitEngine.waitForStaleness(WebElement)` | Previous element detached / replaced after AJAX reload |
| `WaitEngine.waitForPageLoad()` | `document.readyState === "complete"` |
| `WaitEngine.waitForNetworkIdle()` | No network requests in flight (opt-in; CDP on Chrome/Edge, BiDi on Firefox) |

---

## …without hiding Selenium

`WaitEngine` is a convenience, not a wall:

1. **Native Selenium types**: Methods accept standard Selenium `By` locators and return standard `WebElement` or `Alert` objects.
2. **Full escape hatch**: Need a condition that isn't built in? Pass any native Selenium `ExpectedCondition<T>` into `WaitEngine.wait(...)`:
```java
WaitEngine.wait(ExpectedConditions.numberOfWindowsToBe(2));
```
3. **No hidden magic**: `WaitEngine` uses standard `WebDriverWait` and `ExpectedConditions` under the hood. It simply standardizes the timeout, wire protocol, and error reporting across your entire suite.

---

## Wait strategy comparison

| Synchronization Method | Timeout Source | Re-checks Stale Elements | Network-Aware | Test Code Verbosity |
|---|---|---|---|---|
| `Thread.sleep()` | Hardcoded | No | No | Low, but adds dead wait time |
| Scattered `WebDriverWait` | Hardcoded per call | Manual handling needed | No (requires custom CDP wiring) | High boilerplate |
| `WaitEngine` | `selenium-boot.yml` | **Yes** | Opt-in (`waitForNetworkIdle` on CDP/BiDi) | Low, static one-liners |
| Auto-waiting `Locator` (`$()`, `getBy*`) | `selenium-boot.yml` | **Yes** | DOM-readiness (not network-driven) | **Zero explicit wait code** |

*(Note: Auto-waiting locators synchronize on DOM readiness — element presence, visibility, and clickability. When awaiting background AJAX/fetch requests without immediate UI mutations, opt into `WaitEngine.waitForNetworkIdle()`.)*

---

## Next steps

- [Selenium Waits (WaitEngine) Guide](/docs/guides/wait-engine) — full condition reference and timeout overrides
- [Configuration Reference](/docs/configuration) — configuring `timeouts.explicit`, `timeouts.pageLoad`, and `timeouts.script`
- [Why accessibility-first locators?](/docs/why/why-accessibility-first-locators) — how semantic locators survive markup changes
- [Why Selenium Boot?](/docs/why/why-selenium-boot) — the overall philosophy

2 changes: 2 additions & 0 deletions docs-site/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const sidebars = {
'why/why-selenium-boot',
'why/why-not-plain-selenium',
'why/why-not-playwright',
'why/why-accessibility-first-locators',
'why/why-wait-engine',
],
},
'getting-started',
Expand Down
Loading